ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Utility functions for comprehensive access logging.
|
|
2
|
+
|
|
3
|
+
This module provides logging utilities adapted from the observability
|
|
4
|
+
module for use within the access_log plugin.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = get_plugin_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def log_request_access(
|
|
17
|
+
request_id: str,
|
|
18
|
+
method: str | None = None,
|
|
19
|
+
path: str | None = None,
|
|
20
|
+
status_code: int | None = None,
|
|
21
|
+
duration_ms: float | None = None,
|
|
22
|
+
client_ip: str | None = None,
|
|
23
|
+
user_agent: str | None = None,
|
|
24
|
+
query: str | None = None,
|
|
25
|
+
error_message: str | None = None,
|
|
26
|
+
**additional_metadata: Any,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Log comprehensive access information for a request.
|
|
29
|
+
|
|
30
|
+
This function generates a unified access log entry with complete request
|
|
31
|
+
metadata including timing, tokens, costs, and any additional context.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
request_id: Request identifier
|
|
35
|
+
method: HTTP method
|
|
36
|
+
path: Request path
|
|
37
|
+
status_code: HTTP status code
|
|
38
|
+
duration_ms: Request duration in milliseconds
|
|
39
|
+
client_ip: Client IP address
|
|
40
|
+
user_agent: User agent string
|
|
41
|
+
query: Query parameters
|
|
42
|
+
error_message: Error message if applicable
|
|
43
|
+
**additional_metadata: Any additional fields to include
|
|
44
|
+
"""
|
|
45
|
+
# Prepare basic log data (always included)
|
|
46
|
+
log_data: dict[str, Any] = {
|
|
47
|
+
"request_id": request_id,
|
|
48
|
+
"method": method,
|
|
49
|
+
"path": path,
|
|
50
|
+
"query": query,
|
|
51
|
+
"client_ip": client_ip,
|
|
52
|
+
"user_agent": user_agent,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Add response-specific fields
|
|
56
|
+
log_data.update(
|
|
57
|
+
{
|
|
58
|
+
"status_code": status_code,
|
|
59
|
+
"duration_ms": duration_ms,
|
|
60
|
+
"duration_seconds": duration_ms / 1000 if duration_ms else None,
|
|
61
|
+
"error_message": error_message,
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Add token and cost metrics if available in metadata
|
|
66
|
+
token_fields = [
|
|
67
|
+
"tokens_input",
|
|
68
|
+
"tokens_output",
|
|
69
|
+
"cache_read_tokens",
|
|
70
|
+
"cache_write_tokens",
|
|
71
|
+
"cost_usd",
|
|
72
|
+
"num_turns",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
for field in token_fields:
|
|
76
|
+
value = additional_metadata.get(field)
|
|
77
|
+
if value is not None:
|
|
78
|
+
log_data[field] = value
|
|
79
|
+
|
|
80
|
+
# Add service and endpoint info
|
|
81
|
+
service_fields = ["endpoint", "model", "streaming", "service_type", "provider"]
|
|
82
|
+
|
|
83
|
+
for field in service_fields:
|
|
84
|
+
value = additional_metadata.get(field)
|
|
85
|
+
if value is not None:
|
|
86
|
+
log_data[field] = value
|
|
87
|
+
|
|
88
|
+
# Add session context metadata if available
|
|
89
|
+
session_fields = [
|
|
90
|
+
"session_id",
|
|
91
|
+
"session_type",
|
|
92
|
+
"session_status",
|
|
93
|
+
"session_age_seconds",
|
|
94
|
+
"session_message_count",
|
|
95
|
+
"session_pool_enabled",
|
|
96
|
+
"session_idle_seconds",
|
|
97
|
+
"session_error_count",
|
|
98
|
+
"session_is_new",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for field in session_fields:
|
|
102
|
+
value = additional_metadata.get(field)
|
|
103
|
+
if value is not None:
|
|
104
|
+
log_data[field] = value
|
|
105
|
+
|
|
106
|
+
# Add rate limit headers if available
|
|
107
|
+
rate_limit_fields = [
|
|
108
|
+
"x-ratelimit-limit",
|
|
109
|
+
"x-ratelimit-remaining",
|
|
110
|
+
"x-ratelimit-reset",
|
|
111
|
+
"anthropic-ratelimit-requests-limit",
|
|
112
|
+
"anthropic-ratelimit-requests-remaining",
|
|
113
|
+
"anthropic-ratelimit-requests-reset",
|
|
114
|
+
"anthropic-ratelimit-tokens-limit",
|
|
115
|
+
"anthropic-ratelimit-tokens-remaining",
|
|
116
|
+
"anthropic-ratelimit-tokens-reset",
|
|
117
|
+
"anthropic_request_id",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
for field in rate_limit_fields:
|
|
121
|
+
value = additional_metadata.get(field)
|
|
122
|
+
if value is not None:
|
|
123
|
+
log_data[field] = value
|
|
124
|
+
|
|
125
|
+
# Add any additional metadata provided
|
|
126
|
+
log_data.update(additional_metadata)
|
|
127
|
+
|
|
128
|
+
# Remove None values to keep log clean
|
|
129
|
+
log_data = {k: v for k, v in log_data.items() if v is not None}
|
|
130
|
+
|
|
131
|
+
# Log with appropriate level
|
|
132
|
+
bound_logger = logger.bind(**log_data)
|
|
133
|
+
|
|
134
|
+
if error_message:
|
|
135
|
+
bound_logger.warning("access_log", exc_info=additional_metadata.get("error"))
|
|
136
|
+
else:
|
|
137
|
+
is_streaming = additional_metadata.get("streaming", False)
|
|
138
|
+
is_streaming_complete = (
|
|
139
|
+
additional_metadata.get("event_type", "") == "streaming_complete"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if not is_streaming or is_streaming_complete:
|
|
143
|
+
bound_logger.info("access_log")
|
|
144
|
+
else:
|
|
145
|
+
# If streaming is true, and not streaming_complete log as debug
|
|
146
|
+
bound_logger.info("access_log_streaming_start")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def log_request_start(
|
|
150
|
+
request_id: str,
|
|
151
|
+
method: str,
|
|
152
|
+
path: str,
|
|
153
|
+
client_ip: str | None = None,
|
|
154
|
+
user_agent: str | None = None,
|
|
155
|
+
query: str | None = None,
|
|
156
|
+
**additional_metadata: Any,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Log request start event with basic information.
|
|
159
|
+
|
|
160
|
+
This is used for early/hook logging when full context isn't available yet.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
request_id: Request identifier
|
|
164
|
+
method: HTTP method
|
|
165
|
+
path: Request path
|
|
166
|
+
client_ip: Client IP address
|
|
167
|
+
user_agent: User agent string
|
|
168
|
+
query: Query parameters
|
|
169
|
+
**additional_metadata: Any additional fields to include
|
|
170
|
+
"""
|
|
171
|
+
log_data: dict[str, Any] = {
|
|
172
|
+
"request_id": request_id,
|
|
173
|
+
"method": method,
|
|
174
|
+
"path": path,
|
|
175
|
+
"client_ip": client_ip,
|
|
176
|
+
"user_agent": user_agent,
|
|
177
|
+
"query": query,
|
|
178
|
+
"event_type": "request_start",
|
|
179
|
+
"timestamp": time.time(),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Add any additional metadata
|
|
183
|
+
log_data.update(additional_metadata)
|
|
184
|
+
|
|
185
|
+
# Remove None values
|
|
186
|
+
log_data = {k: v for k, v in log_data.items() if v is not None}
|
|
187
|
+
|
|
188
|
+
logger.debug("access_log_start", **log_data)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def log_provider_access(
|
|
192
|
+
request_id: str,
|
|
193
|
+
provider: str,
|
|
194
|
+
method: str,
|
|
195
|
+
url: str,
|
|
196
|
+
status_code: int | None = None,
|
|
197
|
+
duration_ms: float | None = None,
|
|
198
|
+
error_message: str | None = None,
|
|
199
|
+
**additional_metadata: Any,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Log provider access information.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
request_id: Request identifier
|
|
205
|
+
provider: Provider name
|
|
206
|
+
method: HTTP method
|
|
207
|
+
url: Provider URL
|
|
208
|
+
status_code: Response status code
|
|
209
|
+
duration_ms: Request duration in milliseconds
|
|
210
|
+
error_message: Error message if applicable
|
|
211
|
+
**additional_metadata: Any additional fields to include
|
|
212
|
+
"""
|
|
213
|
+
log_data: dict[str, Any] = {
|
|
214
|
+
"request_id": request_id,
|
|
215
|
+
"provider": provider,
|
|
216
|
+
"method": method,
|
|
217
|
+
"url": url,
|
|
218
|
+
"status_code": status_code,
|
|
219
|
+
"duration_ms": duration_ms,
|
|
220
|
+
"duration_seconds": duration_ms / 1000 if duration_ms else None,
|
|
221
|
+
"error_message": error_message,
|
|
222
|
+
"event_type": "provider_access",
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Add token and cost metrics if available
|
|
226
|
+
token_fields = [
|
|
227
|
+
"tokens_input",
|
|
228
|
+
"tokens_output",
|
|
229
|
+
"cache_read_tokens",
|
|
230
|
+
"cache_write_tokens",
|
|
231
|
+
"cost_usd",
|
|
232
|
+
"model",
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
for field in token_fields:
|
|
236
|
+
value = additional_metadata.get(field)
|
|
237
|
+
if value is not None:
|
|
238
|
+
log_data[field] = value
|
|
239
|
+
|
|
240
|
+
# Add any additional metadata
|
|
241
|
+
log_data.update(additional_metadata)
|
|
242
|
+
|
|
243
|
+
# Remove None values
|
|
244
|
+
log_data = {k: v for k, v in log_data.items() if v is not None}
|
|
245
|
+
|
|
246
|
+
# Log with appropriate level
|
|
247
|
+
bound_logger = logger.bind(**log_data)
|
|
248
|
+
|
|
249
|
+
if error_message:
|
|
250
|
+
bound_logger.warning(
|
|
251
|
+
"provider_access_log", exc_info=additional_metadata.get("error")
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
bound_logger.info("provider_access_log")
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
4
|
+
from ccproxy.core.plugins import (
|
|
5
|
+
PluginManifest,
|
|
6
|
+
SystemPluginFactory,
|
|
7
|
+
SystemPluginRuntime,
|
|
8
|
+
)
|
|
9
|
+
from ccproxy.core.plugins.hooks import HookRegistry
|
|
10
|
+
from ccproxy.plugins.analytics.ingest import AnalyticsIngestService
|
|
11
|
+
from ccproxy.services.container import ServiceContainer
|
|
12
|
+
|
|
13
|
+
from .config import AccessLogConfig
|
|
14
|
+
from .hook import AccessLogHook
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = get_plugin_logger()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AccessLogRuntime(SystemPluginRuntime):
|
|
21
|
+
"""Runtime for access log plugin.
|
|
22
|
+
|
|
23
|
+
Integrates with the Hook system to receive and log events.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, manifest: PluginManifest):
|
|
27
|
+
super().__init__(manifest)
|
|
28
|
+
self.hook: AccessLogHook | None = None
|
|
29
|
+
self.config: AccessLogConfig | None = None
|
|
30
|
+
|
|
31
|
+
async def _on_initialize(self) -> None:
|
|
32
|
+
"""Initialize the access logger."""
|
|
33
|
+
if not self.context:
|
|
34
|
+
raise RuntimeError("Context not set")
|
|
35
|
+
|
|
36
|
+
# Get configuration
|
|
37
|
+
config: AccessLogConfig | None = self.context.get("config")
|
|
38
|
+
if config is None or not isinstance(config, AccessLogConfig):
|
|
39
|
+
config = AccessLogConfig()
|
|
40
|
+
self.config = config
|
|
41
|
+
|
|
42
|
+
if not config.enabled:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
self.hook = AccessLogHook(config)
|
|
46
|
+
|
|
47
|
+
hook_registry = self.context.get(HookRegistry)
|
|
48
|
+
|
|
49
|
+
if hook_registry is None or not isinstance(hook_registry, HookRegistry):
|
|
50
|
+
raise RuntimeError("Hook registry not found in context")
|
|
51
|
+
|
|
52
|
+
hook_registry.register(self.hook)
|
|
53
|
+
|
|
54
|
+
# Try to wire analytics ingest service if available
|
|
55
|
+
try:
|
|
56
|
+
registry = self.context.get(ServiceContainer)
|
|
57
|
+
self.hook.ingest_service = registry.get_service(AnalyticsIngestService)
|
|
58
|
+
if not self.hook.ingest_service:
|
|
59
|
+
# optional service
|
|
60
|
+
logger.debug("access_log_analytics_service_not_found")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.warning(
|
|
63
|
+
"access_log_ingest_service_connect_failed", error=str(e), exc_info=e
|
|
64
|
+
)
|
|
65
|
+
#
|
|
66
|
+
# Consolidated ready summary at INFO
|
|
67
|
+
logger.trace(
|
|
68
|
+
"access_log_ready",
|
|
69
|
+
client_enabled=config.client_enabled,
|
|
70
|
+
provider_enabled=config.provider_enabled,
|
|
71
|
+
client_format=config.client_format,
|
|
72
|
+
client_log_file=config.client_log_file,
|
|
73
|
+
provider_log_file=config.provider_log_file,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def _on_shutdown(self) -> None:
|
|
77
|
+
"""Cleanup on shutdown."""
|
|
78
|
+
# Unregister hook from registry
|
|
79
|
+
if self.hook:
|
|
80
|
+
# Try to get hook registry
|
|
81
|
+
hook_registry = None
|
|
82
|
+
if self.context:
|
|
83
|
+
hook_registry = self.context.get("hook_registry")
|
|
84
|
+
if not hook_registry:
|
|
85
|
+
app = self.context.get("app")
|
|
86
|
+
if (
|
|
87
|
+
app
|
|
88
|
+
and hasattr(app, "state")
|
|
89
|
+
and hasattr(app.state, "hook_registry")
|
|
90
|
+
):
|
|
91
|
+
hook_registry = app.state.hook_registry
|
|
92
|
+
|
|
93
|
+
if hook_registry and isinstance(hook_registry, HookRegistry):
|
|
94
|
+
hook_registry.unregister(self.hook)
|
|
95
|
+
logger.trace("access_log_hook_unregistered")
|
|
96
|
+
|
|
97
|
+
# Close hook (flushes writers)
|
|
98
|
+
await self.hook.close()
|
|
99
|
+
logger.trace("access_log_shutdown")
|
|
100
|
+
|
|
101
|
+
async def _get_health_details(self) -> dict[str, Any]:
|
|
102
|
+
"""Get health check details."""
|
|
103
|
+
config = self.config
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"type": "system",
|
|
107
|
+
"initialized": self.initialized,
|
|
108
|
+
"enabled": config.enabled if config else False,
|
|
109
|
+
"client_enabled": config.client_enabled if config else False,
|
|
110
|
+
"provider_enabled": config.provider_enabled if config else False,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def get_hook(self) -> AccessLogHook | None:
|
|
114
|
+
"""Get the hook instance (for testing or manual integration)."""
|
|
115
|
+
return self.hook
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AccessLogFactory(SystemPluginFactory):
|
|
119
|
+
"""Factory for access log plugin."""
|
|
120
|
+
|
|
121
|
+
def __init__(self) -> None:
|
|
122
|
+
manifest = PluginManifest(
|
|
123
|
+
name="access_log",
|
|
124
|
+
version="0.1.0",
|
|
125
|
+
description="Simple access logging with Common, Combined, and Structured formats",
|
|
126
|
+
is_provider=False,
|
|
127
|
+
config_class=AccessLogConfig,
|
|
128
|
+
# dependencies=["analytics"], # optional, handled at runtime
|
|
129
|
+
)
|
|
130
|
+
super().__init__(manifest)
|
|
131
|
+
|
|
132
|
+
def create_runtime(self) -> AccessLogRuntime:
|
|
133
|
+
return AccessLogRuntime(self.manifest)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Export the factory instance
|
|
137
|
+
factory = AccessLogFactory()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import aiofiles
|
|
6
|
+
|
|
7
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = get_plugin_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AccessLogWriter:
|
|
14
|
+
"""Simple async file writer for access logs.
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
- Async file I/O for performance
|
|
18
|
+
- Optional buffering to reduce I/O operations
|
|
19
|
+
- Thread-safe with asyncio.Lock
|
|
20
|
+
- Auto-creates parent directories
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
log_file: str,
|
|
26
|
+
buffer_size: int = 100,
|
|
27
|
+
flush_interval: float = 1.0,
|
|
28
|
+
):
|
|
29
|
+
"""Initialize the writer.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
log_file: Path to the log file
|
|
33
|
+
buffer_size: Number of entries to buffer before writing
|
|
34
|
+
flush_interval: Time in seconds between automatic flushes
|
|
35
|
+
"""
|
|
36
|
+
self.log_file = Path(log_file)
|
|
37
|
+
self.buffer_size = buffer_size
|
|
38
|
+
self.flush_interval = flush_interval
|
|
39
|
+
|
|
40
|
+
self._buffer: list[str] = []
|
|
41
|
+
self._lock = asyncio.Lock()
|
|
42
|
+
self._flush_task: asyncio.Task[None] | None = None
|
|
43
|
+
self._last_flush = time.time()
|
|
44
|
+
|
|
45
|
+
# Ensure parent directory exists
|
|
46
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
async def write(self, line: str) -> None:
|
|
49
|
+
"""Write a line to the log file.
|
|
50
|
+
|
|
51
|
+
Lines are buffered and written in batches for performance.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
line: The formatted log line to write
|
|
55
|
+
"""
|
|
56
|
+
async with self._lock:
|
|
57
|
+
self._buffer.append(line)
|
|
58
|
+
|
|
59
|
+
# Flush if buffer is full
|
|
60
|
+
if len(self._buffer) >= self.buffer_size:
|
|
61
|
+
await self._flush()
|
|
62
|
+
else:
|
|
63
|
+
# Schedule a flush if not already scheduled
|
|
64
|
+
self._schedule_flush()
|
|
65
|
+
|
|
66
|
+
async def _flush(self) -> None:
|
|
67
|
+
"""Flush the buffer to disk.
|
|
68
|
+
|
|
69
|
+
This method assumes the lock is already held.
|
|
70
|
+
"""
|
|
71
|
+
if not self._buffer:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Write all buffered lines at once
|
|
76
|
+
async with aiofiles.open(self.log_file, "a") as f:
|
|
77
|
+
await f.write("\n".join(self._buffer) + "\n")
|
|
78
|
+
|
|
79
|
+
self._buffer.clear()
|
|
80
|
+
self._last_flush = time.time()
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(
|
|
84
|
+
"access_log_write_error",
|
|
85
|
+
error=str(e),
|
|
86
|
+
log_file=str(self.log_file),
|
|
87
|
+
buffer_size=len(self._buffer),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _schedule_flush(self) -> None:
|
|
91
|
+
"""Schedule an automatic flush after the flush interval."""
|
|
92
|
+
if self._flush_task and not self._flush_task.done():
|
|
93
|
+
return # Already scheduled
|
|
94
|
+
|
|
95
|
+
self._flush_task = asyncio.create_task(self._auto_flush())
|
|
96
|
+
|
|
97
|
+
async def _auto_flush(self) -> None:
|
|
98
|
+
"""Automatically flush the buffer after the flush interval."""
|
|
99
|
+
await asyncio.sleep(self.flush_interval)
|
|
100
|
+
async with self._lock:
|
|
101
|
+
await self._flush()
|
|
102
|
+
|
|
103
|
+
async def close(self) -> None:
|
|
104
|
+
"""Close the writer and flush any remaining data."""
|
|
105
|
+
async with self._lock:
|
|
106
|
+
await self._flush()
|
|
107
|
+
|
|
108
|
+
if self._flush_task and not self._flush_task.done():
|
|
109
|
+
self._flush_task.cancel()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Analytics Plugin
|
|
2
|
+
|
|
3
|
+
Persists structured access logs and serves query APIs for observability data.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
- Ensures DuckDB schemas exist and registers the `access_logs` SQLModel table
|
|
7
|
+
- Publishes an ingest service consumed by the access log hook
|
|
8
|
+
- Adds `/logs` routes for querying, streaming, and inspecting request history
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
- `AnalyticsPluginConfig` toggles collection, retention, and debug logging
|
|
12
|
+
- Requires the `duckdb_storage` plugin to supply the underlying engine
|
|
13
|
+
- Generate defaults with `python3 scripts/generate_config_from_model.py \
|
|
14
|
+
--format toml --plugin analytics --config-class AnalyticsPluginConfig`
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[plugins.analytics]
|
|
18
|
+
# enabled = true
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Related Components
|
|
22
|
+
- `plugin.py`: runtime initialization and service registration
|
|
23
|
+
- `ingest.py`: writes events into DuckDB using SQLModel
|
|
24
|
+
- `routes.py`: FastAPI router for analytics and log queries
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Analytics plugin (logs query/analytics/stream endpoints)."""
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sqlmodel import Session
|
|
9
|
+
|
|
10
|
+
from .models import AccessLog
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnalyticsIngestService:
|
|
14
|
+
"""Ingest access logs directly via SQLModel.
|
|
15
|
+
|
|
16
|
+
This service accepts a SQLAlchemy/SQLModel engine and writes AccessLog rows
|
|
17
|
+
without delegating to a storage-specific `store_request` API.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, engine: Any | None):
|
|
21
|
+
self._engine = engine
|
|
22
|
+
|
|
23
|
+
async def ingest(self, log_data: dict[str, Any]) -> bool:
|
|
24
|
+
"""Normalize payload and persist using SQLModel.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
log_data: Access log fields captured by hooks
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True on success, False otherwise
|
|
31
|
+
"""
|
|
32
|
+
if not self._engine:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
# Normalize timestamp to datetime
|
|
36
|
+
ts_value = log_data.get("timestamp", time.time())
|
|
37
|
+
if isinstance(ts_value, int | float):
|
|
38
|
+
ts_dt = datetime.fromtimestamp(ts_value)
|
|
39
|
+
else:
|
|
40
|
+
ts_dt = ts_value
|
|
41
|
+
|
|
42
|
+
# Prefer explicit endpoint then path
|
|
43
|
+
endpoint = log_data.get("endpoint", log_data.get("path", ""))
|
|
44
|
+
|
|
45
|
+
# Map incoming dict to AccessLog fields; defaults keep schema stable
|
|
46
|
+
row = AccessLog(
|
|
47
|
+
request_id=str(log_data.get("request_id", "")),
|
|
48
|
+
timestamp=ts_dt,
|
|
49
|
+
method=str(log_data.get("method", "")),
|
|
50
|
+
endpoint=str(endpoint),
|
|
51
|
+
path=str(log_data.get("path", "")),
|
|
52
|
+
query=str(log_data.get("query", "")),
|
|
53
|
+
client_ip=str(log_data.get("client_ip", "")),
|
|
54
|
+
user_agent=str(log_data.get("user_agent", "")),
|
|
55
|
+
service_type=str(log_data.get("service_type", "access_log")),
|
|
56
|
+
provider=str(log_data.get("provider", "")),
|
|
57
|
+
model=str(log_data.get("model", "")),
|
|
58
|
+
streaming=bool(log_data.get("streaming", False)),
|
|
59
|
+
status_code=int(log_data.get("status_code", 200)),
|
|
60
|
+
duration_ms=float(log_data.get("duration_ms", 0.0)),
|
|
61
|
+
duration_seconds=float(
|
|
62
|
+
log_data.get("duration_seconds", log_data.get("duration_ms", 0.0))
|
|
63
|
+
)
|
|
64
|
+
/ 1000.0
|
|
65
|
+
if "duration_seconds" not in log_data
|
|
66
|
+
else float(log_data.get("duration_seconds", 0.0)),
|
|
67
|
+
tokens_input=int(log_data.get("tokens_input", 0)),
|
|
68
|
+
tokens_output=int(log_data.get("tokens_output", 0)),
|
|
69
|
+
cache_read_tokens=int(log_data.get("cache_read_tokens", 0)),
|
|
70
|
+
cache_write_tokens=int(log_data.get("cache_write_tokens", 0)),
|
|
71
|
+
cost_usd=float(log_data.get("cost_usd", 0.0)),
|
|
72
|
+
cost_sdk_usd=float(log_data.get("cost_sdk_usd", 0.0)),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Execute the DB write in a thread to avoid blocking the event loop
|
|
77
|
+
return await asyncio.to_thread(self._insert_sync, row)
|
|
78
|
+
except Exception:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def _insert_sync(self, row: AccessLog) -> bool:
|
|
82
|
+
with Session(self._engine) as session:
|
|
83
|
+
session.add(row)
|
|
84
|
+
session.commit()
|
|
85
|
+
return True
|