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,97 @@
|
|
|
1
|
+
"""Access log schema and payload definitions (owned by analytics)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlmodel import Field, SQLModel
|
|
8
|
+
from typing_extensions import TypedDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AccessLog(SQLModel, table=True):
|
|
12
|
+
"""Access log model for storing request/response data."""
|
|
13
|
+
|
|
14
|
+
__tablename__ = "access_logs"
|
|
15
|
+
|
|
16
|
+
# Core request identification
|
|
17
|
+
request_id: str = Field(primary_key=True)
|
|
18
|
+
timestamp: datetime = Field(default_factory=datetime.now, index=True)
|
|
19
|
+
|
|
20
|
+
# Request details
|
|
21
|
+
method: str
|
|
22
|
+
endpoint: str
|
|
23
|
+
path: str
|
|
24
|
+
query: str = Field(default="")
|
|
25
|
+
client_ip: str
|
|
26
|
+
user_agent: str
|
|
27
|
+
|
|
28
|
+
# Service and model info
|
|
29
|
+
service_type: str
|
|
30
|
+
provider: str = Field(default="")
|
|
31
|
+
model: str
|
|
32
|
+
streaming: bool = Field(default=False)
|
|
33
|
+
|
|
34
|
+
# Response details
|
|
35
|
+
status_code: int
|
|
36
|
+
duration_ms: float
|
|
37
|
+
duration_seconds: float
|
|
38
|
+
|
|
39
|
+
# Token and cost tracking
|
|
40
|
+
tokens_input: int = Field(default=0)
|
|
41
|
+
tokens_output: int = Field(default=0)
|
|
42
|
+
cache_read_tokens: int = Field(default=0)
|
|
43
|
+
cache_write_tokens: int = Field(default=0)
|
|
44
|
+
cost_usd: float = Field(default=0.0)
|
|
45
|
+
cost_sdk_usd: float = Field(default=0.0)
|
|
46
|
+
num_turns: int = Field(default=0)
|
|
47
|
+
|
|
48
|
+
# Session context metadata
|
|
49
|
+
session_type: str = Field(default="")
|
|
50
|
+
session_status: str = Field(default="")
|
|
51
|
+
session_age_seconds: float = Field(default=0.0)
|
|
52
|
+
session_message_count: int = Field(default=0)
|
|
53
|
+
session_client_id: str = Field(default="")
|
|
54
|
+
session_pool_enabled: bool = Field(default=False)
|
|
55
|
+
session_idle_seconds: float = Field(default=0.0)
|
|
56
|
+
session_error_count: int = Field(default=0)
|
|
57
|
+
session_is_new: bool = Field(default=True)
|
|
58
|
+
|
|
59
|
+
# SQLModel provides its own config typing; avoid overriding with Pydantic ConfigDict
|
|
60
|
+
# from_attributes=True is not required for SQLModel usage here
|
|
61
|
+
# Keep default SQLModel config to satisfy mypy type expectations
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AccessLogPayload(TypedDict, total=False):
|
|
65
|
+
"""TypedDict for access log data payloads."""
|
|
66
|
+
|
|
67
|
+
request_id: str
|
|
68
|
+
timestamp: int | float | datetime
|
|
69
|
+
method: str
|
|
70
|
+
endpoint: str
|
|
71
|
+
path: str
|
|
72
|
+
query: str
|
|
73
|
+
client_ip: str
|
|
74
|
+
user_agent: str
|
|
75
|
+
service_type: str
|
|
76
|
+
provider: str
|
|
77
|
+
model: str
|
|
78
|
+
streaming: bool
|
|
79
|
+
status_code: int
|
|
80
|
+
duration_ms: float
|
|
81
|
+
duration_seconds: float
|
|
82
|
+
tokens_input: int
|
|
83
|
+
tokens_output: int
|
|
84
|
+
cache_read_tokens: int
|
|
85
|
+
cache_write_tokens: int
|
|
86
|
+
cost_usd: float
|
|
87
|
+
cost_sdk_usd: float
|
|
88
|
+
num_turns: int
|
|
89
|
+
session_type: str
|
|
90
|
+
session_status: str
|
|
91
|
+
session_age_seconds: float
|
|
92
|
+
session_message_count: int
|
|
93
|
+
session_client_id: str
|
|
94
|
+
session_pool_enabled: bool
|
|
95
|
+
session_idle_seconds: float
|
|
96
|
+
session_error_count: int
|
|
97
|
+
session_is_new: bool
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
4
|
+
from ccproxy.core.plugins import (
|
|
5
|
+
PluginManifest,
|
|
6
|
+
RouteSpec,
|
|
7
|
+
SystemPluginFactory,
|
|
8
|
+
SystemPluginRuntime,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .config import AnalyticsPluginConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = get_plugin_logger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AnalyticsRuntime(SystemPluginRuntime):
|
|
18
|
+
async def _on_initialize(self) -> None:
|
|
19
|
+
# Ensure AccessLog model is registered and table exists on the engine.
|
|
20
|
+
from sqlmodel import SQLModel
|
|
21
|
+
|
|
22
|
+
# Import models to register with SQLModel metadata
|
|
23
|
+
try:
|
|
24
|
+
from . import models as _models # noqa: F401
|
|
25
|
+
except Exception as e: # pragma: no cover - defensive
|
|
26
|
+
logger.error("analytics_models_import_failed", error=str(e))
|
|
27
|
+
raise
|
|
28
|
+
|
|
29
|
+
# Assert model registration in metadata
|
|
30
|
+
table = SQLModel.metadata.tables.get("access_logs")
|
|
31
|
+
if table is None:
|
|
32
|
+
logger.error("access_logs_table_not_in_metadata")
|
|
33
|
+
raise RuntimeError("AccessLog model not registered in SQLModel metadata")
|
|
34
|
+
|
|
35
|
+
# Try to get storage engine via plugin registry service
|
|
36
|
+
engine = None
|
|
37
|
+
try:
|
|
38
|
+
registry = self.context.get("plugin_registry") if self.context else None
|
|
39
|
+
if registry:
|
|
40
|
+
storage = registry.get_service("log_storage")
|
|
41
|
+
engine = getattr(storage, "_engine", None)
|
|
42
|
+
|
|
43
|
+
# Fallback to app.state if needed
|
|
44
|
+
if (engine is None) and self.context and self.context.get("app"):
|
|
45
|
+
app = self.context["app"]
|
|
46
|
+
storage = getattr(app.state, "log_storage", None)
|
|
47
|
+
engine = getattr(storage, "_engine", None)
|
|
48
|
+
except Exception as e: # pragma: no cover - defensive
|
|
49
|
+
logger.warning("analytics_engine_lookup_failed", error=str(e))
|
|
50
|
+
|
|
51
|
+
# If we have an engine, assert table is created (idempotent create_all)
|
|
52
|
+
if engine is not None:
|
|
53
|
+
try:
|
|
54
|
+
SQLModel.metadata.create_all(engine)
|
|
55
|
+
logger.debug("analytics_table_ready", table="access_logs")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error("analytics_table_create_failed", error=str(e))
|
|
58
|
+
raise
|
|
59
|
+
else:
|
|
60
|
+
logger.warning(
|
|
61
|
+
"analytics_no_engine_available",
|
|
62
|
+
message="Storage engine not available during analytics init; table creation skipped",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Register ingest service for access_log hook to call
|
|
66
|
+
try:
|
|
67
|
+
if self.context:
|
|
68
|
+
registry = self.context.get("plugin_registry")
|
|
69
|
+
storage = None
|
|
70
|
+
if registry:
|
|
71
|
+
# Get storage service without importing DuckDB-specific classes
|
|
72
|
+
storage = registry.get_service("log_storage")
|
|
73
|
+
if not storage and self.context.get("app"):
|
|
74
|
+
storage = getattr(self.context["app"].state, "log_storage", None)
|
|
75
|
+
|
|
76
|
+
if storage:
|
|
77
|
+
engine = getattr(storage, "_engine", None)
|
|
78
|
+
else:
|
|
79
|
+
engine = None
|
|
80
|
+
|
|
81
|
+
if engine is not None:
|
|
82
|
+
from .ingest import AnalyticsIngestService
|
|
83
|
+
|
|
84
|
+
ingest_service = AnalyticsIngestService(engine)
|
|
85
|
+
if registry:
|
|
86
|
+
registry.register_service(
|
|
87
|
+
"analytics_ingest", ingest_service, self.manifest.name
|
|
88
|
+
)
|
|
89
|
+
logger.debug("analytics_ingest_service_registered")
|
|
90
|
+
else:
|
|
91
|
+
logger.warning(
|
|
92
|
+
"analytics_ingest_registration_skipped",
|
|
93
|
+
reason="no_engine_available",
|
|
94
|
+
)
|
|
95
|
+
except Exception as e: # pragma: no cover - defensive
|
|
96
|
+
logger.warning("analytics_ingest_registration_failed", error=str(e))
|
|
97
|
+
|
|
98
|
+
logger.debug("analytics_plugin_initialized")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AnalyticsFactory(SystemPluginFactory):
|
|
102
|
+
def __init__(self) -> None:
|
|
103
|
+
from .routes import router as analytics_router
|
|
104
|
+
|
|
105
|
+
manifest = PluginManifest(
|
|
106
|
+
name="analytics",
|
|
107
|
+
version="0.1.0",
|
|
108
|
+
description="Logs query, analytics, and streaming endpoints",
|
|
109
|
+
is_provider=False,
|
|
110
|
+
config_class=AnalyticsPluginConfig,
|
|
111
|
+
provides=["analytics_ingest"],
|
|
112
|
+
dependencies=["duckdb_storage"],
|
|
113
|
+
routes=[RouteSpec(router=analytics_router, prefix="/logs", tags=["logs"])],
|
|
114
|
+
)
|
|
115
|
+
super().__init__(manifest)
|
|
116
|
+
|
|
117
|
+
def create_runtime(self) -> AnalyticsRuntime:
|
|
118
|
+
return AnalyticsRuntime(self.manifest)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
factory = AnalyticsFactory()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
8
|
+
from fastapi.responses import StreamingResponse
|
|
9
|
+
|
|
10
|
+
from ccproxy.auth.dependencies import ConditionalAuthDep
|
|
11
|
+
from ccproxy.core.request_context import get_request_event_stream
|
|
12
|
+
from ccproxy.plugins.duckdb_storage.storage import SimpleDuckDBStorage
|
|
13
|
+
|
|
14
|
+
from .service import AnalyticsService
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
router = APIRouter()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get("/query")
|
|
21
|
+
async def query_logs(
|
|
22
|
+
storage: DuckDBStorageDep,
|
|
23
|
+
auth: ConditionalAuthDep,
|
|
24
|
+
limit: int = Query(1000, ge=1, le=10000, description="Maximum number of results"),
|
|
25
|
+
start_time: float | None = Query(None, description="Start timestamp filter"),
|
|
26
|
+
end_time: float | None = Query(None, description="End timestamp filter"),
|
|
27
|
+
model: str | None = Query(None, description="Model filter"),
|
|
28
|
+
service_type: str | None = Query(None, description="Service type filter"),
|
|
29
|
+
cursor: float | None = Query(
|
|
30
|
+
None, description="Timestamp cursor for pagination (Unix time)"
|
|
31
|
+
),
|
|
32
|
+
order: str = Query(
|
|
33
|
+
"desc", pattern="^(?i)(asc|desc)$", description="Sort order: asc or desc"
|
|
34
|
+
),
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
if not storage:
|
|
37
|
+
raise HTTPException(status_code=503, detail="Storage backend not available")
|
|
38
|
+
if not getattr(storage, "_engine", None):
|
|
39
|
+
raise HTTPException(status_code=503, detail="Storage engine not available")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
svc = AnalyticsService(storage._engine)
|
|
43
|
+
return svc.query_logs(
|
|
44
|
+
limit=limit,
|
|
45
|
+
start_time=start_time,
|
|
46
|
+
end_time=end_time,
|
|
47
|
+
model=model,
|
|
48
|
+
service_type=service_type,
|
|
49
|
+
cursor=cursor,
|
|
50
|
+
order=order,
|
|
51
|
+
)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise HTTPException(status_code=500, detail=f"Query failed: {str(e)}") from e
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get("/analytics")
|
|
57
|
+
async def get_logs_analytics(
|
|
58
|
+
storage: DuckDBStorageDep,
|
|
59
|
+
auth: ConditionalAuthDep,
|
|
60
|
+
start_time: float | None = Query(None, description="Start timestamp (Unix time)"),
|
|
61
|
+
end_time: float | None = Query(None, description="End timestamp (Unix time)"),
|
|
62
|
+
model: str | None = Query(None, description="Filter by model name"),
|
|
63
|
+
service_type: str | None = Query(
|
|
64
|
+
None,
|
|
65
|
+
description="Filter by service type. Supports comma-separated values and !negation",
|
|
66
|
+
),
|
|
67
|
+
hours: int | None = Query(24, ge=1, le=168, description="Hours of data to analyze"),
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
if not storage:
|
|
70
|
+
raise HTTPException(status_code=503, detail="Storage backend not available")
|
|
71
|
+
if not getattr(storage, "_engine", None):
|
|
72
|
+
raise HTTPException(status_code=503, detail="Storage engine not available")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
svc = AnalyticsService(storage._engine)
|
|
76
|
+
analytics = svc.get_analytics(
|
|
77
|
+
start_time=start_time,
|
|
78
|
+
end_time=end_time,
|
|
79
|
+
model=model,
|
|
80
|
+
service_type=service_type,
|
|
81
|
+
hours=hours,
|
|
82
|
+
)
|
|
83
|
+
analytics["query_params"] = {
|
|
84
|
+
"start_time": start_time,
|
|
85
|
+
"end_time": end_time,
|
|
86
|
+
"model": model,
|
|
87
|
+
"service_type": service_type,
|
|
88
|
+
"hours": hours,
|
|
89
|
+
}
|
|
90
|
+
return analytics
|
|
91
|
+
except Exception as e:
|
|
92
|
+
raise HTTPException(
|
|
93
|
+
status_code=500, detail=f"Analytics query failed: {str(e)}"
|
|
94
|
+
) from e
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.get("/stream")
|
|
98
|
+
async def stream_logs(
|
|
99
|
+
request: Request,
|
|
100
|
+
auth: ConditionalAuthDep,
|
|
101
|
+
model: str | None = Query(None, description="Filter by model name"),
|
|
102
|
+
service_type: str | None = Query(None, description="Filter by service type"),
|
|
103
|
+
min_duration_ms: float | None = Query(None, description="Min duration (ms)"),
|
|
104
|
+
max_duration_ms: float | None = Query(None, description="Max duration (ms)"),
|
|
105
|
+
status_code_min: int | None = Query(None, description="Min status code"),
|
|
106
|
+
status_code_max: int | None = Query(None, description="Max status code"),
|
|
107
|
+
) -> StreamingResponse:
|
|
108
|
+
async def event_generator() -> AsyncGenerator[str, None]:
|
|
109
|
+
try:
|
|
110
|
+
async for event in get_request_event_stream():
|
|
111
|
+
data = event
|
|
112
|
+
if model and data.get("model") != model:
|
|
113
|
+
continue
|
|
114
|
+
if service_type and data.get("service_type") != service_type:
|
|
115
|
+
continue
|
|
116
|
+
if min_duration_ms and data.get("duration_ms", 0) < min_duration_ms:
|
|
117
|
+
continue
|
|
118
|
+
if max_duration_ms and data.get("duration_ms", 0) > max_duration_ms:
|
|
119
|
+
continue
|
|
120
|
+
if status_code_min and data.get("status_code", 0) < status_code_min:
|
|
121
|
+
continue
|
|
122
|
+
if status_code_max and data.get("status_code", 0) > status_code_max:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
yield f"data: {data}\n\n"
|
|
126
|
+
except Exception as e: # pragma: no cover - stream errors aren't fatal
|
|
127
|
+
yield f"event: error\ndata: {str(e)}\n\n"
|
|
128
|
+
|
|
129
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@router.post("/reset")
|
|
133
|
+
async def reset_logs(
|
|
134
|
+
storage: DuckDBStorageDep,
|
|
135
|
+
auth: ConditionalAuthDep,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
if not storage:
|
|
138
|
+
raise HTTPException(status_code=503, detail="Storage backend not available")
|
|
139
|
+
if not hasattr(storage, "reset_data"):
|
|
140
|
+
raise HTTPException(
|
|
141
|
+
status_code=501, detail="Reset not supported by storage backend"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
ok = await storage.reset_data()
|
|
145
|
+
if not ok:
|
|
146
|
+
raise HTTPException(status_code=500, detail="Failed to reset logs data")
|
|
147
|
+
return {
|
|
148
|
+
"status": "success",
|
|
149
|
+
"message": "All logs data has been reset",
|
|
150
|
+
"timestamp": time.time(),
|
|
151
|
+
"backend": "duckdb",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def get_duckdb_storage(request: Request) -> SimpleDuckDBStorage | None:
|
|
156
|
+
"""Get DuckDB storage service from app state.
|
|
157
|
+
|
|
158
|
+
The duckdb_storage plugin registers the storage as app.state.log_storage.
|
|
159
|
+
"""
|
|
160
|
+
return getattr(request.app.state, "log_storage", None)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
DuckDBStorageDep = Annotated[SimpleDuckDBStorage | None, Depends(get_duckdb_storage)]
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime as dt
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlmodel import Session, col, func, select
|
|
8
|
+
|
|
9
|
+
from .models import AccessLog
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AnalyticsService:
|
|
13
|
+
"""Encapsulates analytics queries over the AccessLog table."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, engine: Any):
|
|
16
|
+
self._engine = engine
|
|
17
|
+
|
|
18
|
+
def query_logs(
|
|
19
|
+
self,
|
|
20
|
+
limit: int = 1000,
|
|
21
|
+
start_time: float | None = None,
|
|
22
|
+
end_time: float | None = None,
|
|
23
|
+
model: str | None = None,
|
|
24
|
+
service_type: str | None = None,
|
|
25
|
+
cursor: float | None = None,
|
|
26
|
+
order: str = "desc",
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
with Session(self._engine) as session:
|
|
29
|
+
statement = select(AccessLog)
|
|
30
|
+
|
|
31
|
+
start_dt = dt.fromtimestamp(start_time) if start_time else None
|
|
32
|
+
end_dt = dt.fromtimestamp(end_time) if end_time else None
|
|
33
|
+
cursor_dt = dt.fromtimestamp(cursor) if cursor else None
|
|
34
|
+
|
|
35
|
+
if start_dt:
|
|
36
|
+
statement = statement.where(AccessLog.timestamp >= start_dt)
|
|
37
|
+
if end_dt:
|
|
38
|
+
statement = statement.where(AccessLog.timestamp <= end_dt)
|
|
39
|
+
if model:
|
|
40
|
+
statement = statement.where(AccessLog.model == model)
|
|
41
|
+
if service_type:
|
|
42
|
+
statement = statement.where(AccessLog.service_type == service_type)
|
|
43
|
+
|
|
44
|
+
# Cursor-based pagination using timestamp
|
|
45
|
+
# For descending order (newest first): use timestamp < cursor
|
|
46
|
+
# For ascending order (oldest first): use timestamp > cursor
|
|
47
|
+
if cursor_dt:
|
|
48
|
+
if order.lower() == "asc":
|
|
49
|
+
statement = statement.where(AccessLog.timestamp > cursor_dt)
|
|
50
|
+
else:
|
|
51
|
+
statement = statement.where(AccessLog.timestamp < cursor_dt)
|
|
52
|
+
|
|
53
|
+
if order.lower() == "asc":
|
|
54
|
+
statement = statement.order_by(col(AccessLog.timestamp).asc()).limit(
|
|
55
|
+
limit
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
statement = statement.order_by(col(AccessLog.timestamp).desc()).limit(
|
|
59
|
+
limit
|
|
60
|
+
)
|
|
61
|
+
results = session.exec(statement).all()
|
|
62
|
+
payload = [log.model_dump() for log in results]
|
|
63
|
+
|
|
64
|
+
# Compute next cursor from last item in current page
|
|
65
|
+
next_cursor = None
|
|
66
|
+
if results:
|
|
67
|
+
last = results[-1]
|
|
68
|
+
next_cursor = last.timestamp.timestamp()
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"results": payload,
|
|
72
|
+
"limit": limit,
|
|
73
|
+
"count": len(results),
|
|
74
|
+
"order": order.lower(),
|
|
75
|
+
"cursor": cursor,
|
|
76
|
+
"next_cursor": next_cursor,
|
|
77
|
+
"has_more": len(results) == limit,
|
|
78
|
+
"query_time": time.time(),
|
|
79
|
+
"backend": "sqlmodel",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def get_analytics(
|
|
83
|
+
self,
|
|
84
|
+
start_time: float | None = None,
|
|
85
|
+
end_time: float | None = None,
|
|
86
|
+
model: str | None = None,
|
|
87
|
+
service_type: str | None = None,
|
|
88
|
+
hours: int | None = 24,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
if start_time is None and end_time is None and hours:
|
|
91
|
+
end_time = time.time()
|
|
92
|
+
start_time = end_time - (hours * 3600)
|
|
93
|
+
|
|
94
|
+
start_dt = dt.fromtimestamp(start_time) if start_time else None
|
|
95
|
+
end_dt = dt.fromtimestamp(end_time) if end_time else None
|
|
96
|
+
|
|
97
|
+
def build_filters() -> list[Any]:
|
|
98
|
+
conditions: list[Any] = []
|
|
99
|
+
if start_dt:
|
|
100
|
+
conditions.append(AccessLog.timestamp >= start_dt)
|
|
101
|
+
if end_dt:
|
|
102
|
+
conditions.append(AccessLog.timestamp <= end_dt)
|
|
103
|
+
if model:
|
|
104
|
+
conditions.append(AccessLog.model == model)
|
|
105
|
+
if service_type:
|
|
106
|
+
parts = [s.strip() for s in service_type.split(",")]
|
|
107
|
+
include = [p for p in parts if not p.startswith("!")]
|
|
108
|
+
exclude = [p[1:] for p in parts if p.startswith("!")]
|
|
109
|
+
if include:
|
|
110
|
+
conditions.append(col(AccessLog.service_type).in_(include))
|
|
111
|
+
if exclude:
|
|
112
|
+
conditions.append(~col(AccessLog.service_type).in_(exclude))
|
|
113
|
+
return conditions
|
|
114
|
+
|
|
115
|
+
with Session(self._engine) as session:
|
|
116
|
+
filters = build_filters()
|
|
117
|
+
|
|
118
|
+
total_requests = session.exec(
|
|
119
|
+
select(func.count()).select_from(AccessLog).where(*filters)
|
|
120
|
+
).first()
|
|
121
|
+
total_successful_requests = session.exec(
|
|
122
|
+
select(func.count())
|
|
123
|
+
.select_from(AccessLog)
|
|
124
|
+
.where(
|
|
125
|
+
*filters, AccessLog.status_code >= 200, AccessLog.status_code < 400
|
|
126
|
+
)
|
|
127
|
+
).first()
|
|
128
|
+
total_error_requests = session.exec(
|
|
129
|
+
select(func.count())
|
|
130
|
+
.select_from(AccessLog)
|
|
131
|
+
.where(*filters, AccessLog.status_code >= 400)
|
|
132
|
+
).first()
|
|
133
|
+
avg_duration = session.exec(
|
|
134
|
+
select(func.avg(AccessLog.duration_ms))
|
|
135
|
+
.select_from(AccessLog)
|
|
136
|
+
.where(*filters)
|
|
137
|
+
).first()
|
|
138
|
+
total_cost = session.exec(
|
|
139
|
+
select(func.sum(AccessLog.cost_usd))
|
|
140
|
+
.select_from(AccessLog)
|
|
141
|
+
.where(*filters)
|
|
142
|
+
).first()
|
|
143
|
+
total_tokens_input = session.exec(
|
|
144
|
+
select(func.sum(AccessLog.tokens_input))
|
|
145
|
+
.select_from(AccessLog)
|
|
146
|
+
.where(*filters)
|
|
147
|
+
).first()
|
|
148
|
+
total_tokens_output = session.exec(
|
|
149
|
+
select(func.sum(AccessLog.tokens_output))
|
|
150
|
+
.select_from(AccessLog)
|
|
151
|
+
.where(*filters)
|
|
152
|
+
).first()
|
|
153
|
+
total_cache_read_tokens = session.exec(
|
|
154
|
+
select(func.sum(AccessLog.cache_read_tokens))
|
|
155
|
+
.select_from(AccessLog)
|
|
156
|
+
.where(*filters)
|
|
157
|
+
).first()
|
|
158
|
+
total_cache_write_tokens = session.exec(
|
|
159
|
+
select(func.sum(AccessLog.cache_write_tokens))
|
|
160
|
+
.select_from(AccessLog)
|
|
161
|
+
.where(*filters)
|
|
162
|
+
).first()
|
|
163
|
+
|
|
164
|
+
services = session.exec(
|
|
165
|
+
select(AccessLog.service_type).distinct().where(*filters)
|
|
166
|
+
).all()
|
|
167
|
+
breakdown: dict[str, Any] = {}
|
|
168
|
+
for svc in services:
|
|
169
|
+
svc_filters = filters + [AccessLog.service_type == svc]
|
|
170
|
+
svc_count = session.exec(
|
|
171
|
+
select(func.count()).select_from(AccessLog).where(*svc_filters)
|
|
172
|
+
).first()
|
|
173
|
+
svc_success = session.exec(
|
|
174
|
+
select(func.count())
|
|
175
|
+
.select_from(AccessLog)
|
|
176
|
+
.where(
|
|
177
|
+
*svc_filters,
|
|
178
|
+
AccessLog.status_code >= 200,
|
|
179
|
+
AccessLog.status_code < 400,
|
|
180
|
+
)
|
|
181
|
+
).first()
|
|
182
|
+
svc_error = session.exec(
|
|
183
|
+
select(func.count())
|
|
184
|
+
.select_from(AccessLog)
|
|
185
|
+
.where(*svc_filters, AccessLog.status_code >= 400)
|
|
186
|
+
).first()
|
|
187
|
+
svc_avg = session.exec(
|
|
188
|
+
select(func.avg(AccessLog.duration_ms))
|
|
189
|
+
.select_from(AccessLog)
|
|
190
|
+
.where(*svc_filters)
|
|
191
|
+
).first()
|
|
192
|
+
svc_cost = session.exec(
|
|
193
|
+
select(func.sum(AccessLog.cost_usd))
|
|
194
|
+
.select_from(AccessLog)
|
|
195
|
+
.where(*svc_filters)
|
|
196
|
+
).first()
|
|
197
|
+
svc_in = session.exec(
|
|
198
|
+
select(func.sum(AccessLog.tokens_input))
|
|
199
|
+
.select_from(AccessLog)
|
|
200
|
+
.where(*svc_filters)
|
|
201
|
+
).first()
|
|
202
|
+
svc_out = session.exec(
|
|
203
|
+
select(func.sum(AccessLog.tokens_output))
|
|
204
|
+
.select_from(AccessLog)
|
|
205
|
+
.where(*svc_filters)
|
|
206
|
+
).first()
|
|
207
|
+
svc_cr = session.exec(
|
|
208
|
+
select(func.sum(AccessLog.cache_read_tokens))
|
|
209
|
+
.select_from(AccessLog)
|
|
210
|
+
.where(*svc_filters)
|
|
211
|
+
).first()
|
|
212
|
+
svc_cw = session.exec(
|
|
213
|
+
select(func.sum(AccessLog.cache_write_tokens))
|
|
214
|
+
.select_from(AccessLog)
|
|
215
|
+
.where(*svc_filters)
|
|
216
|
+
).first()
|
|
217
|
+
|
|
218
|
+
breakdown[str(svc)] = {
|
|
219
|
+
"request_count": svc_count or 0,
|
|
220
|
+
"successful_requests": svc_success or 0,
|
|
221
|
+
"error_requests": svc_error or 0,
|
|
222
|
+
"success_rate": (svc_success or 0) / (svc_count or 1) * 100
|
|
223
|
+
if svc_count
|
|
224
|
+
else 0,
|
|
225
|
+
"error_rate": (svc_error or 0) / (svc_count or 1) * 100
|
|
226
|
+
if svc_count
|
|
227
|
+
else 0,
|
|
228
|
+
"avg_duration_ms": svc_avg or 0,
|
|
229
|
+
"total_cost_usd": svc_cost or 0,
|
|
230
|
+
"total_tokens_input": svc_in or 0,
|
|
231
|
+
"total_tokens_output": svc_out or 0,
|
|
232
|
+
"total_cache_read_tokens": svc_cr or 0,
|
|
233
|
+
"total_cache_write_tokens": svc_cw or 0,
|
|
234
|
+
"total_tokens_all": (svc_in or 0)
|
|
235
|
+
+ (svc_out or 0)
|
|
236
|
+
+ (svc_cr or 0)
|
|
237
|
+
+ (svc_cw or 0),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"summary": {
|
|
242
|
+
"total_requests": total_requests or 0,
|
|
243
|
+
"total_successful_requests": total_successful_requests or 0,
|
|
244
|
+
"total_error_requests": total_error_requests or 0,
|
|
245
|
+
"avg_duration_ms": avg_duration or 0,
|
|
246
|
+
"total_cost_usd": total_cost or 0,
|
|
247
|
+
"total_tokens_input": total_tokens_input or 0,
|
|
248
|
+
"total_tokens_output": total_tokens_output or 0,
|
|
249
|
+
"total_cache_read_tokens": total_cache_read_tokens or 0,
|
|
250
|
+
"total_cache_write_tokens": total_cache_write_tokens or 0,
|
|
251
|
+
"total_tokens_all": (total_tokens_input or 0)
|
|
252
|
+
+ (total_tokens_output or 0)
|
|
253
|
+
+ (total_cache_read_tokens or 0)
|
|
254
|
+
+ (total_cache_write_tokens or 0),
|
|
255
|
+
},
|
|
256
|
+
"token_analytics": {
|
|
257
|
+
"input_tokens": total_tokens_input or 0,
|
|
258
|
+
"output_tokens": total_tokens_output or 0,
|
|
259
|
+
"cache_read_tokens": total_cache_read_tokens or 0,
|
|
260
|
+
"cache_write_tokens": total_cache_write_tokens or 0,
|
|
261
|
+
"total_tokens": (total_tokens_input or 0)
|
|
262
|
+
+ (total_tokens_output or 0)
|
|
263
|
+
+ (total_cache_read_tokens or 0)
|
|
264
|
+
+ (total_cache_write_tokens or 0),
|
|
265
|
+
},
|
|
266
|
+
"request_analytics": {
|
|
267
|
+
"total_requests": total_requests or 0,
|
|
268
|
+
"successful_requests": total_successful_requests or 0,
|
|
269
|
+
"error_requests": total_error_requests or 0,
|
|
270
|
+
"success_rate": (total_successful_requests or 0)
|
|
271
|
+
/ (total_requests or 1)
|
|
272
|
+
* 100
|
|
273
|
+
if total_requests
|
|
274
|
+
else 0,
|
|
275
|
+
"error_rate": (total_error_requests or 0)
|
|
276
|
+
/ (total_requests or 1)
|
|
277
|
+
* 100
|
|
278
|
+
if total_requests
|
|
279
|
+
else 0,
|
|
280
|
+
},
|
|
281
|
+
"service_type_breakdown": breakdown,
|
|
282
|
+
"query_time": time.time(),
|
|
283
|
+
"backend": "sqlmodel",
|
|
284
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Claude API Plugin
|
|
2
|
+
|
|
3
|
+
Connects CCProxy to Anthropic's Claude HTTP API with detection, health, and metrics.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
- Wraps `ClaudeAPIAdapter` for chat, tool, and streaming requests
|
|
7
|
+
- Uses the detection service to discover CLI headers and available models
|
|
8
|
+
- Emits streaming metrics and standardized health checks via the hook registry
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
- `ClaudeAPISettings` defines base URLs, model cards, and auth manager name
|
|
12
|
+
- Works with `oauth_claude` or the credential balancer for token management
|
|
13
|
+
- Generate defaults with `python3 scripts/generate_config_from_model.py \
|
|
14
|
+
--format toml --plugin claude_api --config-class ClaudeAPISettings`
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[plugins.claude_api]
|
|
18
|
+
# enabled = true
|
|
19
|
+
# base_url = "https://api.anthropic.com"
|
|
20
|
+
# auth_type = "oauth"
|
|
21
|
+
# supports_streaming = true
|
|
22
|
+
# include_sdk_content_as_xml = false
|
|
23
|
+
# system_prompt_injection_mode = "minimal"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Related Components
|
|
27
|
+
- `adapter.py`: HTTP client for Anthropic endpoints
|
|
28
|
+
- `detection_service.py`: CLI and capability discovery helpers
|
|
29
|
+
- `routes.py`: FastAPI router mounted under `/claude/api`
|