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,633 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import contextlib
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import Mapping, Sequence
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, cast
|
|
17
|
+
|
|
18
|
+
from sqlalchemy import delete, insert
|
|
19
|
+
from sqlalchemy import select as sa_select
|
|
20
|
+
from sqlalchemy.engine import Engine
|
|
21
|
+
from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError
|
|
22
|
+
from sqlmodel import Session, SQLModel, create_engine, func
|
|
23
|
+
|
|
24
|
+
from ccproxy.core.async_task_manager import create_managed_task
|
|
25
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
logger = get_plugin_logger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SimpleDuckDBStorage:
|
|
32
|
+
"""Simple DuckDB storage with queue-based writes to prevent deadlocks."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, database_path: str | Path = "data/metrics.duckdb"):
|
|
35
|
+
"""Initialize simple DuckDB storage.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
database_path: Path to DuckDB database file
|
|
39
|
+
"""
|
|
40
|
+
self.database_path = Path(database_path)
|
|
41
|
+
self._engine: Engine | None = None
|
|
42
|
+
self._initialized: bool = False
|
|
43
|
+
self._write_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
|
44
|
+
self._background_worker_task: asyncio.Task[None] | None = None
|
|
45
|
+
self._shutdown_event = asyncio.Event()
|
|
46
|
+
# Sentinel to wake the background worker immediately on shutdown
|
|
47
|
+
self._sentinel: object = object()
|
|
48
|
+
|
|
49
|
+
async def initialize(self) -> None:
|
|
50
|
+
"""Initialize the storage backend."""
|
|
51
|
+
if self._initialized:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# Ensure data directory exists
|
|
56
|
+
self.database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Create SQLModel engine
|
|
59
|
+
self._engine = create_engine(f"duckdb:///{self.database_path}")
|
|
60
|
+
|
|
61
|
+
# Create schema using SQLModel (synchronous in main thread)
|
|
62
|
+
self._create_schema_sync()
|
|
63
|
+
|
|
64
|
+
# Start background worker for queue processing
|
|
65
|
+
self._background_worker_task = await create_managed_task(
|
|
66
|
+
self._background_worker(),
|
|
67
|
+
name="duckdb_background_worker",
|
|
68
|
+
creator="SimpleDuckDBStorage",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self._initialized = True
|
|
72
|
+
logger.debug(
|
|
73
|
+
"simple_duckdb_initialized", database_path=str(self.database_path)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
except OSError as e:
|
|
77
|
+
logger.error("simple_duckdb_init_io_error", error=str(e), exc_info=e)
|
|
78
|
+
raise
|
|
79
|
+
except SQLAlchemyError as e:
|
|
80
|
+
logger.error("simple_duckdb_init_db_error", error=str(e), exc_info=e)
|
|
81
|
+
raise
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error("simple_duckdb_init_error", error=str(e), exc_info=e)
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
def optimize(self) -> None:
|
|
87
|
+
"""Run PRAGMA optimize on the database engine if available.
|
|
88
|
+
|
|
89
|
+
This is a lightweight maintenance step to improve performance and
|
|
90
|
+
reclaim space in DuckDB. Safe to call on file-backed databases.
|
|
91
|
+
"""
|
|
92
|
+
if not self._engine:
|
|
93
|
+
return
|
|
94
|
+
try:
|
|
95
|
+
with self._engine.connect() as conn:
|
|
96
|
+
conn.exec_driver_sql("PRAGMA optimize")
|
|
97
|
+
logger.debug("duckdb_optimize_completed")
|
|
98
|
+
except Exception as e: # pragma: no cover - non-critical maintenance
|
|
99
|
+
logger.warning("duckdb_optimize_failed", error=str(e), exc_info=e)
|
|
100
|
+
|
|
101
|
+
def _create_schema_sync(self) -> None:
|
|
102
|
+
"""Create database schema using SQLModel (synchronous)."""
|
|
103
|
+
if not self._engine:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
# Create tables using SQLModel metadata.
|
|
108
|
+
# Note: AccessLog model must be imported by the access_log plugin prior to this call.
|
|
109
|
+
SQLModel.metadata.create_all(self._engine)
|
|
110
|
+
logger.debug("duckdb_schema_created")
|
|
111
|
+
|
|
112
|
+
except SQLAlchemyError as e:
|
|
113
|
+
logger.error("simple_duckdb_schema_db_error", error=str(e), exc_info=e)
|
|
114
|
+
raise
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error("simple_duckdb_schema_error", error=str(e), exc_info=e)
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
async def _ensure_query_column(self) -> None:
|
|
120
|
+
"""Ensure query column exists in the access_logs table.
|
|
121
|
+
|
|
122
|
+
Note: This method uses schema introspection to safely check for columns.
|
|
123
|
+
The table schema is managed by SQLModel, so this is primarily for
|
|
124
|
+
backwards compatibility with existing databases.
|
|
125
|
+
"""
|
|
126
|
+
if not self._engine:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# SQLModel automatically handles schema creation through metadata.create_all()
|
|
131
|
+
# This method is kept for backwards compatibility but no longer uses raw SQL
|
|
132
|
+
logger.debug("query_column_ensured_via_sqlmodel_schema")
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.warning("query_column_check_error", error=str(e), exc_info=e)
|
|
136
|
+
# Continue without failing - SQLModel handles schema management
|
|
137
|
+
|
|
138
|
+
async def store_request(self, data: Mapping[str, Any]) -> bool:
|
|
139
|
+
"""Store a single request log entry asynchronously via queue.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
data: Request data to store
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if queued successfully
|
|
146
|
+
"""
|
|
147
|
+
if not self._initialized:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# Add to queue for background processing
|
|
152
|
+
await self._write_queue.put(dict(data))
|
|
153
|
+
return True
|
|
154
|
+
except asyncio.QueueFull as e:
|
|
155
|
+
logger.error(
|
|
156
|
+
"queue_store_full_error",
|
|
157
|
+
error=str(e),
|
|
158
|
+
request_id=data.get("request_id"),
|
|
159
|
+
exc_info=e,
|
|
160
|
+
)
|
|
161
|
+
return False
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(
|
|
164
|
+
"queue_store_error",
|
|
165
|
+
error=str(e),
|
|
166
|
+
request_id=data.get("request_id"),
|
|
167
|
+
exc_info=e,
|
|
168
|
+
)
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
async def _background_worker(self) -> None:
|
|
172
|
+
"""Background worker to process queued write operations sequentially."""
|
|
173
|
+
logger.debug("duckdb_background_worker_started")
|
|
174
|
+
|
|
175
|
+
while not self._shutdown_event.is_set():
|
|
176
|
+
try:
|
|
177
|
+
# Wait for either a queue item or shutdown with timeout
|
|
178
|
+
try:
|
|
179
|
+
data = await asyncio.wait_for(self._write_queue.get(), timeout=1.0)
|
|
180
|
+
except TimeoutError:
|
|
181
|
+
continue # Check shutdown event and continue
|
|
182
|
+
|
|
183
|
+
# We successfully got an item, so we need to mark it done
|
|
184
|
+
try:
|
|
185
|
+
# If we receive a sentinel item, break out quickly on shutdown
|
|
186
|
+
if data is self._sentinel:
|
|
187
|
+
self._write_queue.task_done()
|
|
188
|
+
break
|
|
189
|
+
success = self._store_request_sync(data)
|
|
190
|
+
if success:
|
|
191
|
+
logger.debug(
|
|
192
|
+
"queue_processed_successfully",
|
|
193
|
+
request_id=data.get("request_id"),
|
|
194
|
+
)
|
|
195
|
+
except SQLAlchemyError as e:
|
|
196
|
+
logger.error(
|
|
197
|
+
"background_worker_db_error",
|
|
198
|
+
error=str(e),
|
|
199
|
+
request_id=data.get("request_id"),
|
|
200
|
+
exc_info=e,
|
|
201
|
+
)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(
|
|
204
|
+
"background_worker_error",
|
|
205
|
+
error=str(e),
|
|
206
|
+
request_id=data.get("request_id"),
|
|
207
|
+
exc_info=e,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Always mark the task as done for regular items, regardless of success/failure
|
|
211
|
+
if data is not self._sentinel:
|
|
212
|
+
self._write_queue.task_done()
|
|
213
|
+
|
|
214
|
+
except asyncio.CancelledError as e:
|
|
215
|
+
logger.info("background_worker_cancelled", exc_info=e)
|
|
216
|
+
break
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(
|
|
219
|
+
"background_worker_unexpected_error",
|
|
220
|
+
error=str(e),
|
|
221
|
+
exc_info=e,
|
|
222
|
+
)
|
|
223
|
+
# Continue processing other items
|
|
224
|
+
|
|
225
|
+
# Process any remaining items in the queue during shutdown
|
|
226
|
+
logger.debug("processing_remaining_queue_items_on_shutdown")
|
|
227
|
+
while not self._write_queue.empty():
|
|
228
|
+
try:
|
|
229
|
+
# Get remaining items without timeout during shutdown
|
|
230
|
+
data = self._write_queue.get_nowait()
|
|
231
|
+
|
|
232
|
+
# Process the queued write operation synchronously
|
|
233
|
+
try:
|
|
234
|
+
success = self._store_request_sync(data)
|
|
235
|
+
if success:
|
|
236
|
+
logger.debug(
|
|
237
|
+
"shutdown_queue_processed_successfully",
|
|
238
|
+
request_id=data.get("request_id"),
|
|
239
|
+
)
|
|
240
|
+
except SQLAlchemyError as e:
|
|
241
|
+
logger.error(
|
|
242
|
+
"shutdown_background_worker_db_error",
|
|
243
|
+
error=str(e),
|
|
244
|
+
request_id=data.get("request_id"),
|
|
245
|
+
exc_info=e,
|
|
246
|
+
)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(
|
|
249
|
+
"shutdown_background_worker_error",
|
|
250
|
+
error=str(e),
|
|
251
|
+
request_id=data.get("request_id"),
|
|
252
|
+
exc_info=e,
|
|
253
|
+
)
|
|
254
|
+
# Note: No task_done() call needed for get_nowait() items
|
|
255
|
+
|
|
256
|
+
except asyncio.QueueEmpty:
|
|
257
|
+
# No more items to process
|
|
258
|
+
break
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.error(
|
|
261
|
+
"shutdown_background_worker_unexpected_error",
|
|
262
|
+
error=str(e),
|
|
263
|
+
exc_info=e,
|
|
264
|
+
)
|
|
265
|
+
# Continue processing other items
|
|
266
|
+
|
|
267
|
+
logger.debug("duckdb_background_worker_stopped")
|
|
268
|
+
|
|
269
|
+
def _store_request_sync(self, data: dict[str, Any]) -> bool:
|
|
270
|
+
"""Synchronous version of store_request for thread pool execution."""
|
|
271
|
+
try:
|
|
272
|
+
# Convert Unix timestamp to datetime if needed
|
|
273
|
+
timestamp_value = data.get("timestamp", time.time())
|
|
274
|
+
if isinstance(timestamp_value, int | float):
|
|
275
|
+
timestamp_dt = datetime.fromtimestamp(timestamp_value)
|
|
276
|
+
else:
|
|
277
|
+
timestamp_dt = timestamp_value
|
|
278
|
+
|
|
279
|
+
# Store using SQLAlchemy core insert via SQLModel metadata
|
|
280
|
+
values = {
|
|
281
|
+
"request_id": data.get("request_id", ""),
|
|
282
|
+
"timestamp": timestamp_dt,
|
|
283
|
+
"method": data.get("method", ""),
|
|
284
|
+
"endpoint": data.get("endpoint", ""),
|
|
285
|
+
"path": data.get("path", data.get("endpoint", "")),
|
|
286
|
+
"query": data.get("query", ""),
|
|
287
|
+
"client_ip": data.get("client_ip", ""),
|
|
288
|
+
"user_agent": data.get("user_agent", ""),
|
|
289
|
+
"service_type": data.get("service_type", ""),
|
|
290
|
+
"provider": data.get("provider", ""),
|
|
291
|
+
"model": data.get("model", ""),
|
|
292
|
+
"streaming": data.get("streaming", False),
|
|
293
|
+
"status_code": data.get("status_code", 200),
|
|
294
|
+
"duration_ms": data.get("duration_ms", 0.0),
|
|
295
|
+
"duration_seconds": data.get("duration_seconds", 0.0),
|
|
296
|
+
"tokens_input": data.get("tokens_input", 0),
|
|
297
|
+
"tokens_output": data.get("tokens_output", 0),
|
|
298
|
+
"cache_read_tokens": data.get("cache_read_tokens", 0),
|
|
299
|
+
"cache_write_tokens": data.get("cache_write_tokens", 0),
|
|
300
|
+
"cost_usd": data.get("cost_usd", 0.0),
|
|
301
|
+
"cost_sdk_usd": data.get("cost_sdk_usd", 0.0),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
table = SQLModel.metadata.tables.get("access_logs")
|
|
305
|
+
if table is None:
|
|
306
|
+
raise RuntimeError(
|
|
307
|
+
"access_logs table not registered; ensure analytics plugin is enabled"
|
|
308
|
+
)
|
|
309
|
+
with Session(self._engine) as session:
|
|
310
|
+
try:
|
|
311
|
+
_ = cast(Any, session).exec(insert(table).values(values))
|
|
312
|
+
session.commit()
|
|
313
|
+
except (OperationalError, IntegrityError, SQLAlchemyError) as e:
|
|
314
|
+
# Fallback for older schemas without the 'provider' column
|
|
315
|
+
msg = str(e)
|
|
316
|
+
if "provider" in values and (
|
|
317
|
+
"provider" in msg.lower()
|
|
318
|
+
or "no column" in msg.lower()
|
|
319
|
+
or "unknown" in msg.lower()
|
|
320
|
+
):
|
|
321
|
+
safe_values = {
|
|
322
|
+
k: v for k, v in values.items() if k != "provider"
|
|
323
|
+
}
|
|
324
|
+
session.rollback()
|
|
325
|
+
_ = cast(Any, session).exec(insert(table).values(safe_values))
|
|
326
|
+
session.commit()
|
|
327
|
+
else:
|
|
328
|
+
raise
|
|
329
|
+
|
|
330
|
+
logger.info(
|
|
331
|
+
"simple_duckdb_store_success",
|
|
332
|
+
request_id=data.get("request_id"),
|
|
333
|
+
service_type=data.get("service_type"),
|
|
334
|
+
model=data.get("model"),
|
|
335
|
+
)
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
except IntegrityError as e:
|
|
339
|
+
logger.error(
|
|
340
|
+
"simple_duckdb_store_integrity_error",
|
|
341
|
+
error=str(e),
|
|
342
|
+
request_id=data.get("request_id"),
|
|
343
|
+
exc_info=e,
|
|
344
|
+
)
|
|
345
|
+
return False
|
|
346
|
+
except OperationalError as e:
|
|
347
|
+
logger.error(
|
|
348
|
+
"simple_duckdb_store_operational_error",
|
|
349
|
+
error=str(e),
|
|
350
|
+
request_id=data.get("request_id"),
|
|
351
|
+
exc_info=e,
|
|
352
|
+
)
|
|
353
|
+
return False
|
|
354
|
+
except SQLAlchemyError as e:
|
|
355
|
+
logger.error(
|
|
356
|
+
"simple_duckdb_store_db_error",
|
|
357
|
+
error=str(e),
|
|
358
|
+
request_id=data.get("request_id"),
|
|
359
|
+
exc_info=e,
|
|
360
|
+
)
|
|
361
|
+
return False
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(
|
|
364
|
+
"simple_duckdb_store_error",
|
|
365
|
+
error=str(e),
|
|
366
|
+
request_id=data.get("request_id"),
|
|
367
|
+
exc_info=e,
|
|
368
|
+
)
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
async def store_batch(self, metrics: Sequence[dict[str, Any]]) -> bool:
|
|
372
|
+
"""Store a batch of request logs.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
metrics: List of metric data entries
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
True if stored successfully
|
|
379
|
+
"""
|
|
380
|
+
if not self._initialized or not self._engine:
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
rows = []
|
|
385
|
+
for data in metrics:
|
|
386
|
+
timestamp_value = data.get("timestamp", time.time())
|
|
387
|
+
timestamp_dt = (
|
|
388
|
+
datetime.fromtimestamp(timestamp_value)
|
|
389
|
+
if isinstance(timestamp_value, int | float)
|
|
390
|
+
else timestamp_value
|
|
391
|
+
)
|
|
392
|
+
rows.append(
|
|
393
|
+
{
|
|
394
|
+
"request_id": data.get("request_id", ""),
|
|
395
|
+
"timestamp": timestamp_dt,
|
|
396
|
+
"method": data.get("method", ""),
|
|
397
|
+
"endpoint": data.get("endpoint", ""),
|
|
398
|
+
"path": data.get("path", data.get("endpoint", "")),
|
|
399
|
+
"query": data.get("query", ""),
|
|
400
|
+
"client_ip": data.get("client_ip", ""),
|
|
401
|
+
"user_agent": data.get("user_agent", ""),
|
|
402
|
+
"service_type": data.get("service_type", ""),
|
|
403
|
+
"provider": data.get("provider", ""),
|
|
404
|
+
"model": data.get("model", ""),
|
|
405
|
+
"streaming": data.get("streaming", False),
|
|
406
|
+
"status_code": data.get("status_code", 200),
|
|
407
|
+
"duration_ms": data.get("duration_ms", 0.0),
|
|
408
|
+
"duration_seconds": data.get("duration_seconds", 0.0),
|
|
409
|
+
"tokens_input": data.get("tokens_input", 0),
|
|
410
|
+
"tokens_output": data.get("tokens_output", 0),
|
|
411
|
+
"cache_read_tokens": data.get("cache_read_tokens", 0),
|
|
412
|
+
"cache_write_tokens": data.get("cache_write_tokens", 0),
|
|
413
|
+
"cost_usd": data.get("cost_usd", 0.0),
|
|
414
|
+
"cost_sdk_usd": data.get("cost_sdk_usd", 0.0),
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
table = SQLModel.metadata.tables.get("access_logs")
|
|
419
|
+
if table is None:
|
|
420
|
+
raise RuntimeError(
|
|
421
|
+
"access_logs table not registered; ensure analytics plugin is enabled"
|
|
422
|
+
)
|
|
423
|
+
with Session(self._engine) as session:
|
|
424
|
+
cast(Any, session).exec(insert(table), rows)
|
|
425
|
+
session.commit()
|
|
426
|
+
|
|
427
|
+
logger.info(
|
|
428
|
+
"simple_duckdb_batch_store_success",
|
|
429
|
+
batch_size=len(metrics),
|
|
430
|
+
service_types=[m.get("service_type", "") for m in metrics[:3]],
|
|
431
|
+
request_ids=[m.get("request_id", "") for m in metrics[:3]],
|
|
432
|
+
)
|
|
433
|
+
return True
|
|
434
|
+
|
|
435
|
+
except IntegrityError as e:
|
|
436
|
+
logger.error(
|
|
437
|
+
"simple_duckdb_store_batch_integrity_error",
|
|
438
|
+
error=str(e),
|
|
439
|
+
metric_count=len(metrics),
|
|
440
|
+
exc_info=e,
|
|
441
|
+
)
|
|
442
|
+
return False
|
|
443
|
+
except OperationalError as e:
|
|
444
|
+
logger.error(
|
|
445
|
+
"simple_duckdb_store_batch_operational_error",
|
|
446
|
+
error=str(e),
|
|
447
|
+
metric_count=len(metrics),
|
|
448
|
+
exc_info=e,
|
|
449
|
+
)
|
|
450
|
+
return False
|
|
451
|
+
except SQLAlchemyError as e:
|
|
452
|
+
logger.error(
|
|
453
|
+
"simple_duckdb_store_batch_db_error",
|
|
454
|
+
error=str(e),
|
|
455
|
+
metric_count=len(metrics),
|
|
456
|
+
exc_info=e,
|
|
457
|
+
)
|
|
458
|
+
return False
|
|
459
|
+
except Exception as e:
|
|
460
|
+
logger.error(
|
|
461
|
+
"simple_duckdb_store_batch_error",
|
|
462
|
+
error=str(e),
|
|
463
|
+
metric_count=len(metrics),
|
|
464
|
+
exc_info=e,
|
|
465
|
+
)
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
async def store(self, metric: dict[str, Any]) -> bool:
|
|
469
|
+
"""Store single metric.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
metric: Metric data to store
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
True if stored successfully
|
|
476
|
+
"""
|
|
477
|
+
return await self.store_batch([metric])
|
|
478
|
+
|
|
479
|
+
async def close(self) -> None:
|
|
480
|
+
"""Close the database connection and stop background worker."""
|
|
481
|
+
# Signal shutdown to background worker
|
|
482
|
+
self._shutdown_event.set()
|
|
483
|
+
|
|
484
|
+
# Wake up background worker immediately if it's waiting on queue.get()
|
|
485
|
+
with contextlib.suppress(Exception):
|
|
486
|
+
self._write_queue.put_nowait(self._sentinel) # type: ignore[arg-type]
|
|
487
|
+
|
|
488
|
+
# Wait for background worker to finish
|
|
489
|
+
if self._background_worker_task:
|
|
490
|
+
try:
|
|
491
|
+
await asyncio.wait_for(self._background_worker_task, timeout=5.0)
|
|
492
|
+
except TimeoutError:
|
|
493
|
+
logger.warning("background_worker_shutdown_timeout")
|
|
494
|
+
self._background_worker_task.cancel()
|
|
495
|
+
except asyncio.CancelledError:
|
|
496
|
+
logger.info("background_worker_shutdown_cancelled")
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.error(
|
|
499
|
+
"background_worker_shutdown_error", error=str(e), exc_info=e
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Process remaining items in queue (with timeout)
|
|
503
|
+
try:
|
|
504
|
+
await asyncio.wait_for(self._write_queue.join(), timeout=2.0)
|
|
505
|
+
except TimeoutError:
|
|
506
|
+
logger.warning(
|
|
507
|
+
"queue_drain_timeout", remaining_items=self._write_queue.qsize()
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if self._engine:
|
|
511
|
+
try:
|
|
512
|
+
self._engine.dispose()
|
|
513
|
+
except SQLAlchemyError as e:
|
|
514
|
+
logger.error(
|
|
515
|
+
"simple_duckdb_engine_close_db_error", error=str(e), exc_info=e
|
|
516
|
+
)
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.error(
|
|
519
|
+
"simple_duckdb_engine_close_error", error=str(e), exc_info=e
|
|
520
|
+
)
|
|
521
|
+
finally:
|
|
522
|
+
self._engine = None
|
|
523
|
+
|
|
524
|
+
self._initialized = False
|
|
525
|
+
|
|
526
|
+
def is_enabled(self) -> bool:
|
|
527
|
+
"""Check if storage is enabled and available."""
|
|
528
|
+
return self._initialized
|
|
529
|
+
|
|
530
|
+
async def health_check(self) -> dict[str, Any]:
|
|
531
|
+
"""Get health status of the storage backend."""
|
|
532
|
+
if not self._initialized:
|
|
533
|
+
return {
|
|
534
|
+
"status": "not_initialized",
|
|
535
|
+
"enabled": False,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try:
|
|
539
|
+
if self._engine:
|
|
540
|
+
# Run the synchronous database operation in a thread pool
|
|
541
|
+
access_log_count = await asyncio.to_thread(self._health_check_sync)
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
"status": "healthy",
|
|
545
|
+
"enabled": True,
|
|
546
|
+
"database_path": str(self.database_path),
|
|
547
|
+
"access_log_count": access_log_count,
|
|
548
|
+
"backend": "sqlmodel",
|
|
549
|
+
}
|
|
550
|
+
else:
|
|
551
|
+
return {
|
|
552
|
+
"status": "no_connection",
|
|
553
|
+
"enabled": False,
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
except SQLAlchemyError as e:
|
|
557
|
+
return {
|
|
558
|
+
"status": "unhealthy",
|
|
559
|
+
"enabled": False,
|
|
560
|
+
"error": str(e),
|
|
561
|
+
"error_type": "database",
|
|
562
|
+
}
|
|
563
|
+
except Exception as e:
|
|
564
|
+
return {
|
|
565
|
+
"status": "unhealthy",
|
|
566
|
+
"enabled": False,
|
|
567
|
+
"error": str(e),
|
|
568
|
+
"error_type": "unknown",
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
def _health_check_sync(self) -> int:
|
|
572
|
+
"""Synchronous version of health check for thread pool execution."""
|
|
573
|
+
with Session(self._engine) as session:
|
|
574
|
+
table = SQLModel.metadata.tables.get("access_logs")
|
|
575
|
+
if table is None:
|
|
576
|
+
return 0
|
|
577
|
+
statement = sa_select(func.count()).select_from(table)
|
|
578
|
+
return cast(Any, session).exec(statement).first() or 0
|
|
579
|
+
|
|
580
|
+
async def reset_data(self) -> bool:
|
|
581
|
+
"""Reset all data in the storage (useful for testing/debugging).
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
True if reset was successful
|
|
585
|
+
"""
|
|
586
|
+
if not self._initialized or not self._engine:
|
|
587
|
+
return False
|
|
588
|
+
|
|
589
|
+
try:
|
|
590
|
+
# Run the reset operation in a thread pool
|
|
591
|
+
return await asyncio.to_thread(self._reset_data_sync)
|
|
592
|
+
except SQLAlchemyError as e:
|
|
593
|
+
logger.error("simple_duckdb_reset_db_error", error=str(e), exc_info=e)
|
|
594
|
+
return False
|
|
595
|
+
except Exception as e:
|
|
596
|
+
logger.error("simple_duckdb_reset_error", error=str(e), exc_info=e)
|
|
597
|
+
return False
|
|
598
|
+
|
|
599
|
+
def _reset_data_sync(self) -> bool:
|
|
600
|
+
"""Synchronous version of reset_data for thread pool execution.
|
|
601
|
+
|
|
602
|
+
Uses safe SQLModel ORM operations instead of raw SQL to prevent injection.
|
|
603
|
+
"""
|
|
604
|
+
try:
|
|
605
|
+
table = SQLModel.metadata.tables.get("access_logs")
|
|
606
|
+
if table is None:
|
|
607
|
+
return True
|
|
608
|
+
with Session(self._engine) as session:
|
|
609
|
+
_ = cast(Any, session).exec(delete(table))
|
|
610
|
+
session.commit()
|
|
611
|
+
|
|
612
|
+
logger.info("simple_duckdb_reset_success")
|
|
613
|
+
return True
|
|
614
|
+
except SQLAlchemyError as e:
|
|
615
|
+
logger.error("simple_duckdb_reset_sync_db_error", error=str(e), exc_info=e)
|
|
616
|
+
return False
|
|
617
|
+
except Exception as e:
|
|
618
|
+
logger.error("simple_duckdb_reset_sync_error", error=str(e), exc_info=e)
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
async def wait_for_queue_processing(self, timeout: float = 5.0) -> None:
|
|
622
|
+
"""Wait for all queued items to be processed by the background worker.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
timeout: Maximum time to wait in seconds
|
|
626
|
+
|
|
627
|
+
Raises:
|
|
628
|
+
asyncio.TimeoutError: If processing doesn't complete within timeout
|
|
629
|
+
"""
|
|
630
|
+
if not self._initialized or self._shutdown_event.is_set():
|
|
631
|
+
return
|
|
632
|
+
|
|
633
|
+
await asyncio.wait_for(self._write_queue.join(), timeout=timeout)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Max Tokens Plugin
|
|
2
|
+
|
|
3
|
+
Normalizes `max_tokens` fields so provider requests respect model limits.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
- Injects or corrects `max_tokens` / `max_output_tokens` before sending requests
|
|
7
|
+
- Supports enforce mode, provider filtering, and alias-aware model lookups
|
|
8
|
+
- Pulls limits from pricing cache with optional overrides via local JSON files
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
- `MaxTokensConfig` toggles enablement, enforce mode, fallback values, and targets
|
|
12
|
+
- Environment variables follow the `MAX_TOKENS__*` pattern for quick overrides
|
|
13
|
+
- Generate defaults with `python3 scripts/generate_config_from_model.py \
|
|
14
|
+
--format toml --plugin max_tokens --config-class MaxTokensConfig`
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[plugins.max_tokens]
|
|
18
|
+
# enabled = true
|
|
19
|
+
# default_token_limits_file = "ccproxy/plugins/max_tokens/token_limits.json"
|
|
20
|
+
# fallback_max_tokens = 4096
|
|
21
|
+
# apply_to_all_providers = true
|
|
22
|
+
# target_providers = ["claude_api", "claude_sdk", "codex", "copilot"]
|
|
23
|
+
# require_pricing_data = false
|
|
24
|
+
# log_modifications = true
|
|
25
|
+
# enforce_mode = false
|
|
26
|
+
# prioritize_local_file = false
|
|
27
|
+
|
|
28
|
+
[plugins.max_tokens.modification_reasons]
|
|
29
|
+
# missing = "max_tokens was missing from request"
|
|
30
|
+
# invalid = "max_tokens was invalid or too high"
|
|
31
|
+
# exceeded = "max_tokens exceeded model limit"
|
|
32
|
+
# enforced = "max_tokens enforced to model limit (enforce mode)"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Related Components
|
|
36
|
+
- `plugin.py`: runtime lifecycle and hook registration
|
|
37
|
+
- `adapter.py`: hook implementation that edits outbound payloads
|
|
38
|
+
- `service.py`: token limit lookup and caching helpers
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Max tokens plugin for automatic token limit enforcement.
|
|
2
|
+
|
|
3
|
+
This plugin intercepts requests and automatically sets max_tokens based on
|
|
4
|
+
model limits from the pricing data when no max_tokens is provided.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .adapter import MaxTokensAdapter
|
|
8
|
+
from .config import MaxTokensConfig
|
|
9
|
+
from .plugin import factory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["MaxTokensAdapter", "MaxTokensConfig", "factory"]
|