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,763 @@
|
|
|
1
|
+
"""Hook-based access log implementation."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
7
|
+
from ccproxy.core.plugins.hooks import Hook
|
|
8
|
+
from ccproxy.core.plugins.hooks.base import HookContext
|
|
9
|
+
from ccproxy.core.plugins.hooks.events import HookEvent
|
|
10
|
+
|
|
11
|
+
from .config import AccessLogConfig
|
|
12
|
+
from .formatter import AccessLogFormatter
|
|
13
|
+
from .writer import AccessLogWriter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = get_plugin_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AccessLogHook(Hook):
|
|
20
|
+
"""Hook-based access logger implementation.
|
|
21
|
+
|
|
22
|
+
This hook listens to request/response lifecycle events and logs them
|
|
23
|
+
according to the configured format (common, combined, or structured).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name = "access_log"
|
|
27
|
+
events = [
|
|
28
|
+
HookEvent.REQUEST_STARTED,
|
|
29
|
+
HookEvent.REQUEST_COMPLETED,
|
|
30
|
+
HookEvent.REQUEST_FAILED,
|
|
31
|
+
HookEvent.PROVIDER_REQUEST_PREPARED,
|
|
32
|
+
HookEvent.PROVIDER_RESPONSE_RECEIVED,
|
|
33
|
+
HookEvent.PROVIDER_ERROR,
|
|
34
|
+
HookEvent.PROVIDER_STREAM_END,
|
|
35
|
+
]
|
|
36
|
+
priority = (
|
|
37
|
+
750 # HookLayer.OBSERVATION + 50 - Access logging last to capture all data
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: AccessLogConfig | None = None) -> None:
|
|
41
|
+
"""Initialize the access log hook.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: Access log configuration
|
|
45
|
+
"""
|
|
46
|
+
self.config = config or AccessLogConfig()
|
|
47
|
+
self.formatter = AccessLogFormatter()
|
|
48
|
+
|
|
49
|
+
# Create writers based on configuration
|
|
50
|
+
self.client_writer: AccessLogWriter | None = None
|
|
51
|
+
self.provider_writer: AccessLogWriter | None = None
|
|
52
|
+
|
|
53
|
+
if self.config.client_enabled:
|
|
54
|
+
self.client_writer = AccessLogWriter(
|
|
55
|
+
self.config.client_log_file,
|
|
56
|
+
self.config.buffer_size,
|
|
57
|
+
self.config.flush_interval,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if self.config.provider_enabled:
|
|
61
|
+
self.provider_writer = AccessLogWriter(
|
|
62
|
+
self.config.provider_log_file,
|
|
63
|
+
self.config.buffer_size,
|
|
64
|
+
self.config.flush_interval,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Track in-flight requests
|
|
68
|
+
self.client_requests: dict[str, dict[str, Any]] = {}
|
|
69
|
+
self.provider_requests: dict[str, dict[str, Any]] = {}
|
|
70
|
+
# Store streaming metrics until REQUEST_COMPLETED fires
|
|
71
|
+
self._streaming_metrics: dict[str, dict[str, Any]] = {}
|
|
72
|
+
|
|
73
|
+
self.ingest_service: Any | None = None
|
|
74
|
+
|
|
75
|
+
logger.trace(
|
|
76
|
+
"access_log_hook_initialized",
|
|
77
|
+
enabled=self.config.enabled,
|
|
78
|
+
client_enabled=self.config.client_enabled,
|
|
79
|
+
client_format=self.config.client_format,
|
|
80
|
+
provider_enabled=self.config.provider_enabled,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def __call__(self, context: HookContext) -> None:
|
|
84
|
+
"""Handle hook events for access logging.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
context: Hook context with event data
|
|
88
|
+
"""
|
|
89
|
+
if not self.config.enabled:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Map hook events to handler methods
|
|
93
|
+
handlers = {
|
|
94
|
+
HookEvent.REQUEST_STARTED: self._handle_request_start,
|
|
95
|
+
HookEvent.REQUEST_COMPLETED: self._handle_request_complete,
|
|
96
|
+
HookEvent.REQUEST_FAILED: self._handle_request_failed,
|
|
97
|
+
HookEvent.PROVIDER_REQUEST_PREPARED: self._handle_provider_request,
|
|
98
|
+
HookEvent.PROVIDER_RESPONSE_RECEIVED: self._handle_provider_response,
|
|
99
|
+
HookEvent.PROVIDER_ERROR: self._handle_provider_error,
|
|
100
|
+
HookEvent.PROVIDER_STREAM_END: self._handle_provider_stream_end,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
handler = handlers.get(context.event)
|
|
104
|
+
if handler:
|
|
105
|
+
try:
|
|
106
|
+
await handler(context)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(
|
|
109
|
+
"access_log_hook_error",
|
|
110
|
+
hook_event=context.event.value if context.event else "unknown",
|
|
111
|
+
error=str(e),
|
|
112
|
+
exc_info=e,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def _handle_request_start(self, context: HookContext) -> None:
|
|
116
|
+
"""Handle REQUEST_STARTED event."""
|
|
117
|
+
if not self.config.client_enabled:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Extract request data from context
|
|
121
|
+
request_id = context.data.get("request_id", "unknown")
|
|
122
|
+
method = context.data.get("method", "UNKNOWN")
|
|
123
|
+
|
|
124
|
+
# Handle both path and url fields
|
|
125
|
+
path = context.data.get("path", "")
|
|
126
|
+
if not path and "url" in context.data:
|
|
127
|
+
# Extract path from URL
|
|
128
|
+
url = context.data.get("url", "")
|
|
129
|
+
path = self._extract_path(url)
|
|
130
|
+
|
|
131
|
+
query = context.data.get("query", "")
|
|
132
|
+
|
|
133
|
+
# Try to get client_ip from various sources
|
|
134
|
+
client_ip = context.data.get("client_ip", "-")
|
|
135
|
+
if client_ip == "-" and context.request and hasattr(context.request, "client"):
|
|
136
|
+
# Try to get from request object
|
|
137
|
+
client_ip = (
|
|
138
|
+
getattr(context.request.client, "host", "-")
|
|
139
|
+
if context.request.client
|
|
140
|
+
else "-"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Try to get user_agent from headers
|
|
144
|
+
user_agent = context.data.get("user_agent", "-")
|
|
145
|
+
if user_agent == "-":
|
|
146
|
+
headers = context.data.get("headers", {})
|
|
147
|
+
user_agent = headers.get("user-agent", "-")
|
|
148
|
+
|
|
149
|
+
# Check path filters
|
|
150
|
+
if self._should_exclude_path(path):
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# Store request data for later
|
|
154
|
+
# Get current time for timestamp
|
|
155
|
+
current_time = time.time()
|
|
156
|
+
|
|
157
|
+
# Store request data with additional context fields
|
|
158
|
+
request_data = {
|
|
159
|
+
"timestamp": current_time, # Store as float for formatter compatibility
|
|
160
|
+
"method": method,
|
|
161
|
+
"path": path,
|
|
162
|
+
"query": query,
|
|
163
|
+
"client_ip": client_ip,
|
|
164
|
+
"user_agent": user_agent,
|
|
165
|
+
"start_time": current_time,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Add additional context fields if available
|
|
169
|
+
additional_fields = [
|
|
170
|
+
"endpoint",
|
|
171
|
+
"service_type",
|
|
172
|
+
"provider",
|
|
173
|
+
"model",
|
|
174
|
+
"session_id",
|
|
175
|
+
"session_type",
|
|
176
|
+
"streaming",
|
|
177
|
+
]
|
|
178
|
+
for field in additional_fields:
|
|
179
|
+
value = context.data.get(field)
|
|
180
|
+
if value is not None:
|
|
181
|
+
request_data[field] = value
|
|
182
|
+
|
|
183
|
+
self.client_requests[request_id] = request_data
|
|
184
|
+
|
|
185
|
+
async def _handle_request_complete(self, context: HookContext) -> None:
|
|
186
|
+
"""Handle REQUEST_COMPLETED event."""
|
|
187
|
+
if not self.config.client_enabled:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
request_id = context.data.get("request_id", "unknown")
|
|
191
|
+
|
|
192
|
+
# Check if we have the request data
|
|
193
|
+
if request_id not in self.client_requests:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# Check if this is a streaming response by looking for streaming flag
|
|
197
|
+
# For streaming responses, we'll handle logging in PROVIDER_STREAM_END
|
|
198
|
+
# to ensure we have all metrics
|
|
199
|
+
is_streaming = (
|
|
200
|
+
context.data.get("streaming_completed", False)
|
|
201
|
+
or context.data.get("streaming", False)
|
|
202
|
+
or self.client_requests.get(request_id, {}).get("streaming", False)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if is_streaming:
|
|
206
|
+
# Check if we have metrics in metadata (non-streaming response wrapped as streaming)
|
|
207
|
+
has_metrics = False
|
|
208
|
+
if context.metadata:
|
|
209
|
+
# Check if we have token metrics available
|
|
210
|
+
has_metrics = any(
|
|
211
|
+
context.metadata.get(field) is not None
|
|
212
|
+
for field in ["tokens_input", "tokens_output", "cost_usd"]
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if not has_metrics:
|
|
216
|
+
# True streaming response - wait for PROVIDER_STREAM_END
|
|
217
|
+
# Just mark that we got the completion
|
|
218
|
+
if request_id in self.client_requests:
|
|
219
|
+
self.client_requests[request_id]["completion_time"] = time.time()
|
|
220
|
+
self.client_requests[request_id]["status_code"] = context.data.get(
|
|
221
|
+
"response_status", 200
|
|
222
|
+
)
|
|
223
|
+
return
|
|
224
|
+
# If we have metrics, continue to log immediately (non-streaming wrapped as streaming)
|
|
225
|
+
|
|
226
|
+
# For non-streaming responses, log immediately
|
|
227
|
+
# Get and remove request data
|
|
228
|
+
request_data = self.client_requests.pop(request_id)
|
|
229
|
+
|
|
230
|
+
# Calculate duration
|
|
231
|
+
duration_ms = (time.time() - request_data["start_time"]) * 1000
|
|
232
|
+
|
|
233
|
+
# Extract response data
|
|
234
|
+
status_code = context.data.get("status_code", 200)
|
|
235
|
+
body_size = context.data.get("body_size", 0)
|
|
236
|
+
|
|
237
|
+
# Check if we have usage metrics in context metadata
|
|
238
|
+
# These might be available from RequestContext metadata
|
|
239
|
+
usage_metrics = {}
|
|
240
|
+
if context.metadata:
|
|
241
|
+
# Extract any token/cost metrics from metadata
|
|
242
|
+
token_fields = [
|
|
243
|
+
"tokens_input",
|
|
244
|
+
"tokens_output",
|
|
245
|
+
"cache_read_tokens",
|
|
246
|
+
"cache_write_tokens",
|
|
247
|
+
"cost_usd",
|
|
248
|
+
"model",
|
|
249
|
+
]
|
|
250
|
+
for field in token_fields:
|
|
251
|
+
value = context.metadata.get(field)
|
|
252
|
+
if value is not None:
|
|
253
|
+
usage_metrics[field] = value
|
|
254
|
+
|
|
255
|
+
# Merge request and response data
|
|
256
|
+
log_data = {
|
|
257
|
+
**request_data,
|
|
258
|
+
"request_id": request_id,
|
|
259
|
+
"status_code": status_code,
|
|
260
|
+
"body_size": body_size,
|
|
261
|
+
"duration_ms": duration_ms,
|
|
262
|
+
"error": None,
|
|
263
|
+
**usage_metrics, # Include any usage metrics found
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Format and write
|
|
267
|
+
if self.client_writer:
|
|
268
|
+
formatted = self.formatter.format_client(
|
|
269
|
+
log_data, self.config.client_format
|
|
270
|
+
)
|
|
271
|
+
await self.client_writer.write(formatted)
|
|
272
|
+
|
|
273
|
+
# Also log to structured logger
|
|
274
|
+
await self._log_to_structured_logger(log_data, "client")
|
|
275
|
+
|
|
276
|
+
# Ingest into analytics if available
|
|
277
|
+
await self._maybe_ingest(log_data)
|
|
278
|
+
|
|
279
|
+
async def _handle_request_failed(self, context: HookContext) -> None:
|
|
280
|
+
"""Handle REQUEST_FAILED event."""
|
|
281
|
+
if not self.config.client_enabled:
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
request_id = context.data.get("request_id", "unknown")
|
|
285
|
+
|
|
286
|
+
# Check if we have the request data
|
|
287
|
+
if request_id not in self.client_requests:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Get and remove request data
|
|
291
|
+
request_data = self.client_requests.pop(request_id)
|
|
292
|
+
|
|
293
|
+
# Calculate duration
|
|
294
|
+
duration_ms = (time.time() - request_data["start_time"]) * 1000
|
|
295
|
+
|
|
296
|
+
# Extract error information
|
|
297
|
+
error = context.error
|
|
298
|
+
error_message = str(error) if error else "Unknown error"
|
|
299
|
+
status_code = context.data.get("status_code", 500)
|
|
300
|
+
|
|
301
|
+
# Merge request and error data
|
|
302
|
+
log_data = {
|
|
303
|
+
**request_data,
|
|
304
|
+
"request_id": request_id,
|
|
305
|
+
"status_code": status_code,
|
|
306
|
+
"body_size": 0,
|
|
307
|
+
"duration_ms": duration_ms,
|
|
308
|
+
"error": error_message,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Format and write
|
|
312
|
+
if self.client_writer:
|
|
313
|
+
formatted = self.formatter.format_client(
|
|
314
|
+
log_data, self.config.client_format
|
|
315
|
+
)
|
|
316
|
+
await self.client_writer.write(formatted)
|
|
317
|
+
|
|
318
|
+
# Also log to structured logger
|
|
319
|
+
await self._log_to_structured_logger(log_data, "client", error=error_message)
|
|
320
|
+
|
|
321
|
+
# Ingest into analytics if available
|
|
322
|
+
await self._maybe_ingest(log_data)
|
|
323
|
+
|
|
324
|
+
async def _handle_provider_request(self, context: HookContext) -> None:
|
|
325
|
+
"""Handle PROVIDER_REQUEST_PREPARED event."""
|
|
326
|
+
if not self.config.provider_enabled:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
330
|
+
provider = context.provider or "unknown"
|
|
331
|
+
url = context.data.get("url", "")
|
|
332
|
+
method = context.data.get("method", "UNKNOWN")
|
|
333
|
+
|
|
334
|
+
# Store request data for later
|
|
335
|
+
# Get current time for timestamp
|
|
336
|
+
current_time = time.time()
|
|
337
|
+
|
|
338
|
+
self.provider_requests[request_id] = {
|
|
339
|
+
"timestamp": current_time, # Store as float for formatter compatibility
|
|
340
|
+
"provider": provider,
|
|
341
|
+
"method": method,
|
|
342
|
+
"url": url,
|
|
343
|
+
"start_time": current_time,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async def _handle_provider_response(self, context: HookContext) -> None:
|
|
347
|
+
"""Handle PROVIDER_RESPONSE_RECEIVED event."""
|
|
348
|
+
if not self.config.provider_enabled:
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
352
|
+
|
|
353
|
+
# Check if we have the request data
|
|
354
|
+
if request_id not in self.provider_requests:
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
# Get and remove request data
|
|
358
|
+
request_data = self.provider_requests.pop(request_id)
|
|
359
|
+
|
|
360
|
+
# Calculate duration if not provided
|
|
361
|
+
duration_ms = context.data.get("duration_ms", 0)
|
|
362
|
+
if duration_ms == 0:
|
|
363
|
+
duration_ms = (time.time() - request_data["start_time"]) * 1000
|
|
364
|
+
|
|
365
|
+
# Extract response data
|
|
366
|
+
status_code = context.data.get("status_code", 200)
|
|
367
|
+
tokens_input = context.data.get("tokens_input", 0)
|
|
368
|
+
tokens_output = context.data.get("tokens_output", 0)
|
|
369
|
+
cache_read_tokens = context.data.get("cache_read_tokens", 0)
|
|
370
|
+
cache_write_tokens = context.data.get("cache_write_tokens", 0)
|
|
371
|
+
cost_usd = context.data.get("cost_usd", 0.0)
|
|
372
|
+
model = context.data.get("model", "")
|
|
373
|
+
|
|
374
|
+
# Merge request and response data
|
|
375
|
+
log_data = {
|
|
376
|
+
**request_data,
|
|
377
|
+
"request_id": request_id,
|
|
378
|
+
"status_code": status_code,
|
|
379
|
+
"duration_ms": duration_ms,
|
|
380
|
+
"tokens_input": tokens_input,
|
|
381
|
+
"tokens_output": tokens_output,
|
|
382
|
+
"cache_read_tokens": cache_read_tokens,
|
|
383
|
+
"cache_write_tokens": cache_write_tokens,
|
|
384
|
+
"cost_usd": cost_usd,
|
|
385
|
+
"model": model,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Format and write
|
|
389
|
+
if self.provider_writer:
|
|
390
|
+
formatted = self.formatter.format_provider(log_data)
|
|
391
|
+
await self.provider_writer.write(formatted)
|
|
392
|
+
|
|
393
|
+
# Also log to structured logger
|
|
394
|
+
await self._log_to_structured_logger(log_data, "provider")
|
|
395
|
+
|
|
396
|
+
async def _handle_provider_error(self, context: HookContext) -> None:
|
|
397
|
+
"""Handle PROVIDER_ERROR event."""
|
|
398
|
+
if not self.config.provider_enabled:
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
402
|
+
|
|
403
|
+
# Check if we have the request data
|
|
404
|
+
if request_id not in self.provider_requests:
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
# Get and remove request data
|
|
408
|
+
request_data = self.provider_requests.pop(request_id)
|
|
409
|
+
|
|
410
|
+
# Calculate duration
|
|
411
|
+
duration_ms = (time.time() - request_data["start_time"]) * 1000
|
|
412
|
+
|
|
413
|
+
# Extract error information
|
|
414
|
+
error = context.error
|
|
415
|
+
error_message = str(error) if error else "Unknown error"
|
|
416
|
+
status_code = context.data.get("status_code", 500)
|
|
417
|
+
|
|
418
|
+
# Merge request and error data
|
|
419
|
+
log_data = {
|
|
420
|
+
**request_data,
|
|
421
|
+
"request_id": request_id,
|
|
422
|
+
"status_code": status_code,
|
|
423
|
+
"duration_ms": duration_ms,
|
|
424
|
+
"tokens_input": 0,
|
|
425
|
+
"tokens_output": 0,
|
|
426
|
+
"cache_read_tokens": 0,
|
|
427
|
+
"cache_write_tokens": 0,
|
|
428
|
+
"cost_usd": 0.0,
|
|
429
|
+
"model": "",
|
|
430
|
+
"error": error_message,
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# Format and write
|
|
434
|
+
if self.provider_writer:
|
|
435
|
+
formatted = self.formatter.format_provider(log_data)
|
|
436
|
+
await self.provider_writer.write(formatted)
|
|
437
|
+
|
|
438
|
+
# Also log to structured logger
|
|
439
|
+
await self._log_to_structured_logger(log_data, "provider", error=error_message)
|
|
440
|
+
|
|
441
|
+
async def _handle_provider_stream_end(self, context: HookContext) -> None:
|
|
442
|
+
"""Handle PROVIDER_STREAM_END event to capture complete streaming metrics."""
|
|
443
|
+
if not self.config.provider_enabled and not self.config.client_enabled:
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
request_id = context.metadata.get("request_id", "unknown")
|
|
447
|
+
|
|
448
|
+
# Extract usage metrics from the event
|
|
449
|
+
usage_metrics = context.data.get("usage_metrics", {})
|
|
450
|
+
|
|
451
|
+
# Store metrics for logging
|
|
452
|
+
self._streaming_metrics[request_id] = {
|
|
453
|
+
"usage_metrics": usage_metrics,
|
|
454
|
+
"provider": context.provider or context.data.get("provider", "unknown"),
|
|
455
|
+
"url": context.data.get("url", ""),
|
|
456
|
+
"method": context.data.get("method", "POST"),
|
|
457
|
+
"total_chunks": context.data.get("total_chunks", 0),
|
|
458
|
+
"total_bytes": context.data.get("total_bytes", 0),
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# If we have client request data for this streaming request, log it now with metrics
|
|
462
|
+
if self.config.client_enabled and request_id in self.client_requests:
|
|
463
|
+
request_data = self.client_requests.pop(request_id)
|
|
464
|
+
|
|
465
|
+
# Calculate duration
|
|
466
|
+
completion_time = request_data.get("completion_time", time.time())
|
|
467
|
+
duration_ms = (completion_time - request_data["start_time"]) * 1000
|
|
468
|
+
|
|
469
|
+
# Extract metrics (handle both naming conventions)
|
|
470
|
+
tokens_input = usage_metrics.get(
|
|
471
|
+
"input_tokens", usage_metrics.get("tokens_input", 0)
|
|
472
|
+
)
|
|
473
|
+
tokens_output = usage_metrics.get(
|
|
474
|
+
"output_tokens", usage_metrics.get("tokens_output", 0)
|
|
475
|
+
)
|
|
476
|
+
cache_read_tokens = usage_metrics.get(
|
|
477
|
+
"cache_read_input_tokens", usage_metrics.get("cache_read_tokens", 0)
|
|
478
|
+
)
|
|
479
|
+
cache_write_tokens = usage_metrics.get(
|
|
480
|
+
"cache_creation_input_tokens",
|
|
481
|
+
usage_metrics.get("cache_write_tokens", 0),
|
|
482
|
+
)
|
|
483
|
+
cost_usd = usage_metrics.get("cost_usd", 0.0)
|
|
484
|
+
model = usage_metrics.get("model") or request_data.get("model", "")
|
|
485
|
+
|
|
486
|
+
# Build complete log data
|
|
487
|
+
client_log_data = {
|
|
488
|
+
**request_data,
|
|
489
|
+
"request_id": request_id,
|
|
490
|
+
"status_code": request_data.get("status_code", 200),
|
|
491
|
+
"duration_ms": duration_ms,
|
|
492
|
+
"tokens_input": tokens_input,
|
|
493
|
+
"tokens_output": tokens_output,
|
|
494
|
+
"cache_read_tokens": cache_read_tokens,
|
|
495
|
+
"cache_write_tokens": cache_write_tokens,
|
|
496
|
+
"cost_usd": cost_usd,
|
|
497
|
+
"model": model,
|
|
498
|
+
"streaming": True,
|
|
499
|
+
"total_chunks": context.data.get("total_chunks", 0),
|
|
500
|
+
"total_bytes": context.data.get("total_bytes", 0),
|
|
501
|
+
"error": None,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
# Format and write client log
|
|
505
|
+
if self.client_writer:
|
|
506
|
+
formatted = self.formatter.format_client(
|
|
507
|
+
client_log_data, self.config.client_format
|
|
508
|
+
)
|
|
509
|
+
await self.client_writer.write(formatted)
|
|
510
|
+
|
|
511
|
+
# Log to structured logger
|
|
512
|
+
await self._log_to_structured_logger(client_log_data, "client")
|
|
513
|
+
|
|
514
|
+
# Ingest into analytics with full client details (includes IP/UA)
|
|
515
|
+
await self._maybe_ingest(client_log_data)
|
|
516
|
+
|
|
517
|
+
# Extract complete metrics from usage_metrics (handle both naming conventions)
|
|
518
|
+
tokens_input = usage_metrics.get(
|
|
519
|
+
"input_tokens", usage_metrics.get("tokens_input", 0)
|
|
520
|
+
)
|
|
521
|
+
tokens_output = usage_metrics.get(
|
|
522
|
+
"output_tokens", usage_metrics.get("tokens_output", 0)
|
|
523
|
+
)
|
|
524
|
+
cache_read_tokens = usage_metrics.get(
|
|
525
|
+
"cache_read_input_tokens", usage_metrics.get("cache_read_tokens", 0)
|
|
526
|
+
)
|
|
527
|
+
cache_write_tokens = usage_metrics.get(
|
|
528
|
+
"cache_creation_input_tokens", usage_metrics.get("cache_write_tokens", 0)
|
|
529
|
+
)
|
|
530
|
+
cost_usd = usage_metrics.get("cost_usd", 0.0)
|
|
531
|
+
model = usage_metrics.get("model", "")
|
|
532
|
+
|
|
533
|
+
# Get other data from context
|
|
534
|
+
provider = context.provider or context.data.get("provider", "unknown")
|
|
535
|
+
url = context.data.get("url", "")
|
|
536
|
+
method = context.data.get("method", "POST")
|
|
537
|
+
total_chunks = context.data.get("total_chunks", 0)
|
|
538
|
+
total_bytes = context.data.get("total_bytes", 0)
|
|
539
|
+
|
|
540
|
+
# Create log data for streaming complete
|
|
541
|
+
log_data = {
|
|
542
|
+
"timestamp": time.time(),
|
|
543
|
+
"request_id": request_id,
|
|
544
|
+
"provider": provider,
|
|
545
|
+
"method": method,
|
|
546
|
+
"url": url,
|
|
547
|
+
"status_code": 200, # Streaming completion implies success
|
|
548
|
+
"tokens_input": tokens_input,
|
|
549
|
+
"tokens_output": tokens_output,
|
|
550
|
+
"cache_read_tokens": cache_read_tokens,
|
|
551
|
+
"cache_write_tokens": cache_write_tokens,
|
|
552
|
+
"cost_usd": cost_usd,
|
|
553
|
+
"model": model,
|
|
554
|
+
"total_chunks": total_chunks,
|
|
555
|
+
"total_bytes": total_bytes,
|
|
556
|
+
"streaming": True,
|
|
557
|
+
"event_type": "streaming_complete",
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# Format and write to provider log
|
|
561
|
+
if self.provider_writer and self.config.provider_enabled:
|
|
562
|
+
formatted = self.formatter.format_provider(log_data)
|
|
563
|
+
await self.provider_writer.write(formatted)
|
|
564
|
+
|
|
565
|
+
# Log provider streaming metrics captured (for debugging)
|
|
566
|
+
logger.debug(
|
|
567
|
+
"access_log_provider_stream_end_captured",
|
|
568
|
+
request_id=request_id,
|
|
569
|
+
tokens_input=tokens_input,
|
|
570
|
+
tokens_output=tokens_output,
|
|
571
|
+
cost_usd=cost_usd,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# If client request details were not available earlier, we skip ingestion here
|
|
575
|
+
# to avoid emitting incomplete records with missing IP/User-Agent.
|
|
576
|
+
|
|
577
|
+
def _extract_path(self, url: str) -> str:
|
|
578
|
+
"""Extract path from URL.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
url: Full URL or path
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
The path portion of the URL
|
|
585
|
+
"""
|
|
586
|
+
if "://" in url:
|
|
587
|
+
# Full URL - extract path
|
|
588
|
+
parts = url.split("/", 3)
|
|
589
|
+
return "/" + parts[3] if len(parts) > 3 else "/"
|
|
590
|
+
return url
|
|
591
|
+
|
|
592
|
+
def _should_exclude_path(self, path: str) -> bool:
|
|
593
|
+
"""Check if a path should be excluded from logging.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
path: The request path
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
True if the path should be excluded, False otherwise
|
|
600
|
+
"""
|
|
601
|
+
return any(path.startswith(excluded) for excluded in self.config.exclude_paths)
|
|
602
|
+
|
|
603
|
+
async def _maybe_ingest(self, log_data: dict[str, Any]) -> None:
|
|
604
|
+
"""Ingest log data into analytics storage if service is available."""
|
|
605
|
+
try:
|
|
606
|
+
if self.ingest_service and hasattr(self.ingest_service, "ingest"):
|
|
607
|
+
await self.ingest_service.ingest(log_data)
|
|
608
|
+
except Exception as e: # pragma: no cover - non-fatal
|
|
609
|
+
logger.debug("access_log_ingest_failed", error=str(e))
|
|
610
|
+
|
|
611
|
+
async def _log_to_structured_logger(
|
|
612
|
+
self,
|
|
613
|
+
log_data: dict[str, Any],
|
|
614
|
+
log_type: str,
|
|
615
|
+
error: str | None = None,
|
|
616
|
+
) -> None:
|
|
617
|
+
"""Log to structured logger (stdout/stderr).
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
log_data: Log data dictionary
|
|
621
|
+
log_type: Type of log ("client" or "provider")
|
|
622
|
+
error: Error message if applicable
|
|
623
|
+
"""
|
|
624
|
+
# Prepare structured log entry with all available fields
|
|
625
|
+
structured_data = {
|
|
626
|
+
"log_type": log_type,
|
|
627
|
+
"request_id": log_data.get("request_id"),
|
|
628
|
+
"method": log_data.get("method"),
|
|
629
|
+
"path": log_data.get("path"),
|
|
630
|
+
"status_code": log_data.get("status_code"),
|
|
631
|
+
"duration_ms": log_data.get("duration_ms"),
|
|
632
|
+
"client_ip": log_data.get("client_ip"),
|
|
633
|
+
"user_agent": log_data.get("user_agent"),
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
# Add token and cost metrics (available for both client and provider logs)
|
|
637
|
+
token_fields = [
|
|
638
|
+
"tokens_input",
|
|
639
|
+
"tokens_output",
|
|
640
|
+
"cache_read_tokens",
|
|
641
|
+
"cache_write_tokens",
|
|
642
|
+
"cost_usd",
|
|
643
|
+
"model",
|
|
644
|
+
]
|
|
645
|
+
|
|
646
|
+
for field in token_fields:
|
|
647
|
+
value = log_data.get(field)
|
|
648
|
+
if value is not None:
|
|
649
|
+
structured_data[field] = value
|
|
650
|
+
|
|
651
|
+
# Add streaming-specific fields if present
|
|
652
|
+
streaming_fields = ["streaming", "total_chunks", "total_bytes", "event_type"]
|
|
653
|
+
for field in streaming_fields:
|
|
654
|
+
value = log_data.get(field)
|
|
655
|
+
if value is not None:
|
|
656
|
+
structured_data[field] = value
|
|
657
|
+
|
|
658
|
+
# Add service and endpoint info
|
|
659
|
+
service_fields = ["endpoint", "service_type", "provider"]
|
|
660
|
+
for field in service_fields:
|
|
661
|
+
value = log_data.get(field)
|
|
662
|
+
if value is not None:
|
|
663
|
+
structured_data[field] = value
|
|
664
|
+
|
|
665
|
+
# Add session context metadata if available
|
|
666
|
+
session_fields = [
|
|
667
|
+
"session_id",
|
|
668
|
+
"session_type",
|
|
669
|
+
"session_status",
|
|
670
|
+
"session_age_seconds",
|
|
671
|
+
"session_message_count",
|
|
672
|
+
"session_pool_enabled",
|
|
673
|
+
"session_idle_seconds",
|
|
674
|
+
"session_error_count",
|
|
675
|
+
"session_is_new",
|
|
676
|
+
]
|
|
677
|
+
for field in session_fields:
|
|
678
|
+
value = log_data.get(field)
|
|
679
|
+
if value is not None:
|
|
680
|
+
structured_data[field] = value
|
|
681
|
+
|
|
682
|
+
# Add provider-specific URL if this is a provider log
|
|
683
|
+
if log_type == "provider" and "url" not in structured_data:
|
|
684
|
+
url = log_data.get("url")
|
|
685
|
+
if url:
|
|
686
|
+
structured_data["url"] = url
|
|
687
|
+
|
|
688
|
+
# Remove None values to keep log clean
|
|
689
|
+
structured_data = {k: v for k, v in structured_data.items() if v is not None}
|
|
690
|
+
|
|
691
|
+
# Log with appropriate level - event is passed as first argument to logger methods
|
|
692
|
+
if error:
|
|
693
|
+
logger.warning("access_log", error=error, **structured_data)
|
|
694
|
+
else:
|
|
695
|
+
logger.info("access_log", **structured_data)
|
|
696
|
+
|
|
697
|
+
async def _log_streaming_complete(
|
|
698
|
+
self, request_id: str, context: HookContext
|
|
699
|
+
) -> None:
|
|
700
|
+
"""Log streaming completion with full metrics.
|
|
701
|
+
|
|
702
|
+
This is called when REQUEST_COMPLETED fires for a streaming response,
|
|
703
|
+
using the metrics we stored from PROVIDER_STREAM_END.
|
|
704
|
+
"""
|
|
705
|
+
if request_id not in self.client_requests:
|
|
706
|
+
return
|
|
707
|
+
|
|
708
|
+
# Get stored metrics
|
|
709
|
+
metrics_data = self._streaming_metrics.pop(request_id, {})
|
|
710
|
+
usage_metrics = metrics_data.get("usage_metrics", {})
|
|
711
|
+
|
|
712
|
+
# Get the original request data
|
|
713
|
+
request_data = self.client_requests.pop(request_id)
|
|
714
|
+
|
|
715
|
+
# Calculate duration
|
|
716
|
+
duration_ms = (time.time() - request_data["start_time"]) * 1000
|
|
717
|
+
|
|
718
|
+
# Extract metrics
|
|
719
|
+
tokens_input = usage_metrics.get("tokens_input", 0)
|
|
720
|
+
tokens_output = usage_metrics.get("tokens_output", 0)
|
|
721
|
+
cache_read_tokens = usage_metrics.get("cache_read_tokens", 0)
|
|
722
|
+
cache_write_tokens = usage_metrics.get("cache_write_tokens", 0)
|
|
723
|
+
cost_usd = usage_metrics.get("cost_usd", 0.0)
|
|
724
|
+
model = usage_metrics.get("model", "")
|
|
725
|
+
|
|
726
|
+
# Merge request data with streaming metrics
|
|
727
|
+
client_log_data = {
|
|
728
|
+
**request_data,
|
|
729
|
+
"request_id": request_id,
|
|
730
|
+
"status_code": 200,
|
|
731
|
+
"duration_ms": duration_ms,
|
|
732
|
+
"tokens_input": tokens_input,
|
|
733
|
+
"tokens_output": tokens_output,
|
|
734
|
+
"cache_read_tokens": cache_read_tokens,
|
|
735
|
+
"cache_write_tokens": cache_write_tokens,
|
|
736
|
+
"cost_usd": cost_usd,
|
|
737
|
+
"model": model,
|
|
738
|
+
"streaming": True,
|
|
739
|
+
"total_chunks": metrics_data.get("total_chunks", 0),
|
|
740
|
+
"total_bytes": metrics_data.get("total_bytes", 0),
|
|
741
|
+
"error": None,
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
# Format and write client log
|
|
745
|
+
if self.client_writer:
|
|
746
|
+
formatted = self.formatter.format_client(
|
|
747
|
+
client_log_data, self.config.client_format
|
|
748
|
+
)
|
|
749
|
+
await self.client_writer.write(formatted)
|
|
750
|
+
|
|
751
|
+
# Log to structured logger for client
|
|
752
|
+
await self._log_to_structured_logger(client_log_data, "client")
|
|
753
|
+
|
|
754
|
+
logger.info(
|
|
755
|
+
"access_log", **{k: v for k, v in client_log_data.items() if v is not None}
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
async def close(self) -> None:
|
|
759
|
+
"""Close writers and flush any pending data."""
|
|
760
|
+
if self.client_writer:
|
|
761
|
+
await self.client_writer.close()
|
|
762
|
+
if self.provider_writer:
|
|
763
|
+
await self.provider_writer.close()
|