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,552 @@
|
|
|
1
|
+
"""JSON formatter for structured request/response logging."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
from structlog.contextvars import get_merged_contextvars
|
|
14
|
+
|
|
15
|
+
from ccproxy.core.plugins.hooks.types import HookHeaders
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from ccproxy.core.logging import TRACE_LEVEL
|
|
20
|
+
except ImportError:
|
|
21
|
+
TRACE_LEVEL = 5 # Fallback
|
|
22
|
+
|
|
23
|
+
logger = structlog.get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JSONFormatter:
|
|
27
|
+
"""Formats requests/responses as structured JSON for observability."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
log_dir: str = "/tmp/ccproxy/traces",
|
|
32
|
+
verbose_api: bool = True,
|
|
33
|
+
json_logs_enabled: bool = True,
|
|
34
|
+
redact_sensitive: bool = True,
|
|
35
|
+
truncate_body_preview: int = 1024,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize with configuration.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
log_dir: Directory for log files
|
|
41
|
+
verbose_api: Enable verbose API logging
|
|
42
|
+
json_logs_enabled: Enable JSON file logging
|
|
43
|
+
redact_sensitive: Redact sensitive headers
|
|
44
|
+
truncate_body_preview: Max body preview size
|
|
45
|
+
"""
|
|
46
|
+
self.log_dir = log_dir
|
|
47
|
+
self.verbose_api = verbose_api
|
|
48
|
+
self.json_logs_enabled = json_logs_enabled
|
|
49
|
+
self.redact_sensitive = redact_sensitive
|
|
50
|
+
self.truncate_body_preview = truncate_body_preview
|
|
51
|
+
|
|
52
|
+
# Check if TRACE level is enabled
|
|
53
|
+
current_level = (
|
|
54
|
+
logger._context.get("_level", logging.INFO)
|
|
55
|
+
if hasattr(logger, "_context")
|
|
56
|
+
else logging.INFO
|
|
57
|
+
)
|
|
58
|
+
self.trace_enabled = self.verbose_api or current_level <= TRACE_LEVEL
|
|
59
|
+
|
|
60
|
+
# Setup log directory if file logging is enabled
|
|
61
|
+
self.request_log_dir = None
|
|
62
|
+
if self.json_logs_enabled:
|
|
63
|
+
self.request_log_dir = Path(log_dir)
|
|
64
|
+
self.request_log_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_config(cls, config: Any) -> "JSONFormatter":
|
|
68
|
+
"""Create JSONFormatter from a RequestTracerConfig.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
config: RequestTracerConfig instance
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
JSONFormatter instance
|
|
75
|
+
"""
|
|
76
|
+
return cls(
|
|
77
|
+
log_dir=config.get_json_log_dir(),
|
|
78
|
+
verbose_api=config.verbose_api,
|
|
79
|
+
json_logs_enabled=config.json_logs_enabled,
|
|
80
|
+
redact_sensitive=config.redact_sensitive,
|
|
81
|
+
truncate_body_preview=config.truncate_body_preview,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _current_cmd_id(self) -> str | None:
|
|
85
|
+
"""Return current cmd_id from structlog contextvars or env."""
|
|
86
|
+
try:
|
|
87
|
+
ctx = get_merged_contextvars(logger) or {}
|
|
88
|
+
cmd_id = ctx.get("cmd_id")
|
|
89
|
+
except Exception:
|
|
90
|
+
cmd_id = None
|
|
91
|
+
|
|
92
|
+
return str(cmd_id) if cmd_id else None
|
|
93
|
+
|
|
94
|
+
def _compose_file_id(self, request_id: str | None) -> str:
|
|
95
|
+
"""Build filename ID using cmd_id and request_id per rules.
|
|
96
|
+
|
|
97
|
+
- If both cmd_id and request_id exist: "{cmd_id}_{request_id}"
|
|
98
|
+
- If only request_id exists: request_id
|
|
99
|
+
- If only cmd_id exists: cmd_id
|
|
100
|
+
- If neither exists: generate a UUID4
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
ctx = get_merged_contextvars(logger) or {}
|
|
104
|
+
cmd_id = ctx.get("cmd_id")
|
|
105
|
+
except Exception:
|
|
106
|
+
cmd_id = None
|
|
107
|
+
|
|
108
|
+
if cmd_id and request_id:
|
|
109
|
+
return f"{cmd_id}_{request_id}"
|
|
110
|
+
if request_id:
|
|
111
|
+
return request_id
|
|
112
|
+
if cmd_id:
|
|
113
|
+
return str(cmd_id)
|
|
114
|
+
return str(uuid.uuid4())
|
|
115
|
+
|
|
116
|
+
def _compose_file_id_with_timestamp(self, request_id: str | None) -> str:
|
|
117
|
+
"""Build filename ID with timestamp suffix for better organization.
|
|
118
|
+
|
|
119
|
+
Format: {base_id}_{timestamp}_{sequence}
|
|
120
|
+
Where timestamp is in format: YYYYMMDD_HHMMSS_microseconds
|
|
121
|
+
And sequence is a counter to prevent collisions
|
|
122
|
+
"""
|
|
123
|
+
base_id = self._compose_file_id(request_id)
|
|
124
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
125
|
+
|
|
126
|
+
# Add a high-resolution timestamp with nanoseconds for uniqueness
|
|
127
|
+
nanos = time.time_ns() % 1000000 # Get nanosecond portion
|
|
128
|
+
return f"{base_id}_{timestamp}_{nanos:06d}"
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def redact_headers(headers: dict[str, str]) -> dict[str, str]:
|
|
132
|
+
"""Redact sensitive headers for safe logging.
|
|
133
|
+
|
|
134
|
+
- Replaces authorization, x-api-key, cookie values with [REDACTED]
|
|
135
|
+
- Preserves header names for debugging
|
|
136
|
+
- Returns new dict without modifying original
|
|
137
|
+
"""
|
|
138
|
+
sensitive_headers = {
|
|
139
|
+
"authorization",
|
|
140
|
+
"x-api-key",
|
|
141
|
+
"api-key",
|
|
142
|
+
"cookie",
|
|
143
|
+
"x-auth-token",
|
|
144
|
+
"x-secret-key",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
redacted = {}
|
|
148
|
+
for key, value in headers.items():
|
|
149
|
+
if key.lower() in sensitive_headers:
|
|
150
|
+
redacted[key] = "[REDACTED]"
|
|
151
|
+
else:
|
|
152
|
+
redacted[key] = value
|
|
153
|
+
return redacted
|
|
154
|
+
|
|
155
|
+
async def log_request(
|
|
156
|
+
self,
|
|
157
|
+
request_id: str,
|
|
158
|
+
method: str,
|
|
159
|
+
url: str,
|
|
160
|
+
headers: HookHeaders | dict[str, str],
|
|
161
|
+
body: bytes | None,
|
|
162
|
+
request_type: str = "provider", # "client" or "provider"
|
|
163
|
+
context: Any = None, # RequestContext
|
|
164
|
+
hook_type: str | None = None, # Hook type for filename (e.g., "tracer", "http")
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Log structured request data.
|
|
167
|
+
|
|
168
|
+
- Logs at TRACE level with redacted headers
|
|
169
|
+
- Writes to request log file with complete data (if configured)
|
|
170
|
+
"""
|
|
171
|
+
if not self.trace_enabled:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Normalize headers (preserve order/case if dict-like)
|
|
175
|
+
headers_dict = (
|
|
176
|
+
headers.to_dict() if hasattr(headers, "to_dict") else dict(headers)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Log at TRACE level with redacted headers
|
|
180
|
+
log_headers = (
|
|
181
|
+
self.redact_headers(headers_dict) if self.redact_sensitive else headers_dict
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if hasattr(logger, "trace"):
|
|
185
|
+
logger.trace(
|
|
186
|
+
"api_request",
|
|
187
|
+
category="http",
|
|
188
|
+
request_id=request_id,
|
|
189
|
+
method=method,
|
|
190
|
+
url=url,
|
|
191
|
+
headers=log_headers,
|
|
192
|
+
body_size=len(body) if body else 0,
|
|
193
|
+
)
|
|
194
|
+
elif self.verbose_api:
|
|
195
|
+
# Fallback for backward compatibility
|
|
196
|
+
logger.info(
|
|
197
|
+
"api_request",
|
|
198
|
+
category="http",
|
|
199
|
+
request_id=request_id,
|
|
200
|
+
method=method,
|
|
201
|
+
url=url,
|
|
202
|
+
headers=log_headers,
|
|
203
|
+
body_size=len(body) if body else 0,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Write to file if configured
|
|
207
|
+
if self.request_log_dir and self.json_logs_enabled:
|
|
208
|
+
# Build file suffix with hook type
|
|
209
|
+
base_suffix = (
|
|
210
|
+
f"{request_type}_request" if request_type != "provider" else "request"
|
|
211
|
+
)
|
|
212
|
+
if hook_type:
|
|
213
|
+
file_suffix = f"{base_suffix}_{hook_type}"
|
|
214
|
+
else:
|
|
215
|
+
file_suffix = base_suffix
|
|
216
|
+
|
|
217
|
+
base_id = self._compose_file_id_with_timestamp(request_id)
|
|
218
|
+
request_file = self.request_log_dir / f"{base_id}_{file_suffix}.json"
|
|
219
|
+
|
|
220
|
+
# Handle body content - could be bytes, dict/list (from JSON), or string
|
|
221
|
+
body_content = None
|
|
222
|
+
if body is not None:
|
|
223
|
+
if isinstance(body, dict | list):
|
|
224
|
+
# Already parsed JSON object from hook context
|
|
225
|
+
body_content = body
|
|
226
|
+
elif isinstance(body, bytes):
|
|
227
|
+
# Raw bytes - try to parse as JSON first, then string, then base64
|
|
228
|
+
try:
|
|
229
|
+
# First try to decode as UTF-8 string
|
|
230
|
+
body_str = body.decode("utf-8")
|
|
231
|
+
# Then try to parse as JSON
|
|
232
|
+
body_content = json.loads(body_str)
|
|
233
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
234
|
+
# Not JSON, try plain string
|
|
235
|
+
try:
|
|
236
|
+
body_content = body.decode("utf-8", errors="replace")
|
|
237
|
+
except Exception:
|
|
238
|
+
# Last resort: encode as base64
|
|
239
|
+
body_content = {
|
|
240
|
+
"_type": "base64",
|
|
241
|
+
"data": base64.b64encode(body).decode("ascii"),
|
|
242
|
+
}
|
|
243
|
+
elif isinstance(body, str):
|
|
244
|
+
# String body - try to parse as JSON, otherwise keep as string
|
|
245
|
+
try:
|
|
246
|
+
body_content = json.loads(body)
|
|
247
|
+
except json.JSONDecodeError:
|
|
248
|
+
body_content = body
|
|
249
|
+
else:
|
|
250
|
+
# Other type - convert to string
|
|
251
|
+
body_content = str(body)
|
|
252
|
+
|
|
253
|
+
request_data = {
|
|
254
|
+
"request_id": request_id,
|
|
255
|
+
"method": method,
|
|
256
|
+
"url": url,
|
|
257
|
+
"headers": headers_dict, # Full headers in file
|
|
258
|
+
"body": body_content,
|
|
259
|
+
"type": request_type,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Add cmd_id for CLI correlation if present
|
|
263
|
+
cmd_id = self._current_cmd_id()
|
|
264
|
+
if cmd_id:
|
|
265
|
+
request_data["cmd_id"] = cmd_id
|
|
266
|
+
|
|
267
|
+
# Add context data if available
|
|
268
|
+
if context and hasattr(context, "to_dict"):
|
|
269
|
+
try:
|
|
270
|
+
context_data = context.to_dict()
|
|
271
|
+
if context_data:
|
|
272
|
+
request_data["context"] = context_data
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.debug(
|
|
275
|
+
"context_serialization_error",
|
|
276
|
+
error=str(e),
|
|
277
|
+
request_id=request_id,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
request_file.write_text(json.dumps(request_data, indent=2, default=str))
|
|
281
|
+
|
|
282
|
+
async def log_response(
|
|
283
|
+
self,
|
|
284
|
+
request_id: str,
|
|
285
|
+
status: int,
|
|
286
|
+
headers: HookHeaders | dict[str, str],
|
|
287
|
+
body: bytes,
|
|
288
|
+
response_type: str = "provider", # "client" or "provider"
|
|
289
|
+
context: Any = None, # RequestContext
|
|
290
|
+
hook_type: str | None = None, # Hook type for filename (e.g., "tracer", "http")
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Log structured response data.
|
|
293
|
+
|
|
294
|
+
- Logs at TRACE level
|
|
295
|
+
- Truncates body preview for console
|
|
296
|
+
- Handles binary data gracefully
|
|
297
|
+
"""
|
|
298
|
+
if not self.trace_enabled:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
body_preview = self._get_body_preview(body)
|
|
302
|
+
|
|
303
|
+
# Normalize headers (preserve order/case if dict-like)
|
|
304
|
+
headers_dict = (
|
|
305
|
+
headers.to_dict() if hasattr(headers, "to_dict") else dict(headers)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Log at TRACE level
|
|
309
|
+
if hasattr(logger, "trace"):
|
|
310
|
+
logger.trace(
|
|
311
|
+
"api_response",
|
|
312
|
+
category="http",
|
|
313
|
+
request_id=request_id,
|
|
314
|
+
status=status,
|
|
315
|
+
headers=headers_dict,
|
|
316
|
+
body_preview=body_preview,
|
|
317
|
+
body_size=len(body),
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
# Fallback for backward compatibility
|
|
321
|
+
logger.info(
|
|
322
|
+
"api_response",
|
|
323
|
+
category="http",
|
|
324
|
+
request_id=request_id,
|
|
325
|
+
status=status,
|
|
326
|
+
headers=headers_dict,
|
|
327
|
+
body_preview=body_preview,
|
|
328
|
+
body_size=len(body),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Write to file if configured
|
|
332
|
+
if self.request_log_dir and self.json_logs_enabled:
|
|
333
|
+
# Build file suffix with hook type
|
|
334
|
+
base_suffix = (
|
|
335
|
+
f"{response_type}_response"
|
|
336
|
+
if response_type != "provider"
|
|
337
|
+
else "response"
|
|
338
|
+
)
|
|
339
|
+
if hook_type:
|
|
340
|
+
file_suffix = f"{base_suffix}_{hook_type}"
|
|
341
|
+
else:
|
|
342
|
+
file_suffix = base_suffix
|
|
343
|
+
logger.debug(
|
|
344
|
+
"Writing response JSON file",
|
|
345
|
+
request_id=request_id,
|
|
346
|
+
status=status,
|
|
347
|
+
response_type=response_type,
|
|
348
|
+
file_suffix=file_suffix,
|
|
349
|
+
body_type=type(body).__name__,
|
|
350
|
+
body_size=len(body) if body else 0,
|
|
351
|
+
body_preview=body[:100] if body else None,
|
|
352
|
+
)
|
|
353
|
+
base_id = self._compose_file_id_with_timestamp(request_id)
|
|
354
|
+
response_file = self.request_log_dir / f"{base_id}_{file_suffix}.json"
|
|
355
|
+
|
|
356
|
+
# Try to parse body as JSON first, then string, then base64
|
|
357
|
+
body_content: str | dict[str, Any] = ""
|
|
358
|
+
if body:
|
|
359
|
+
try:
|
|
360
|
+
# First try to decode as UTF-8 string
|
|
361
|
+
body_str = body.decode("utf-8")
|
|
362
|
+
# Then try to parse as JSON
|
|
363
|
+
body_content = json.loads(body_str)
|
|
364
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
365
|
+
# Not JSON, try plain string
|
|
366
|
+
try:
|
|
367
|
+
body_content = body.decode("utf-8", errors="replace")
|
|
368
|
+
except Exception:
|
|
369
|
+
# Last resort: encode as base64
|
|
370
|
+
import base64
|
|
371
|
+
|
|
372
|
+
body_content = {
|
|
373
|
+
"_type": "base64",
|
|
374
|
+
"data": base64.b64encode(body).decode("ascii"),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
response_data = {
|
|
378
|
+
"request_id": request_id,
|
|
379
|
+
"status": status,
|
|
380
|
+
"headers": headers_dict,
|
|
381
|
+
"body": body_content,
|
|
382
|
+
"type": response_type,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# Add cmd_id for CLI correlation if present
|
|
386
|
+
cmd_id = self._current_cmd_id()
|
|
387
|
+
if cmd_id:
|
|
388
|
+
response_data["cmd_id"] = cmd_id
|
|
389
|
+
|
|
390
|
+
# Add context data if available (including cost/metrics)
|
|
391
|
+
if context and hasattr(context, "to_dict"):
|
|
392
|
+
try:
|
|
393
|
+
context_data = context.to_dict()
|
|
394
|
+
if context_data:
|
|
395
|
+
response_data["context"] = context_data
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.debug(
|
|
398
|
+
"context_serialization_error",
|
|
399
|
+
error=str(e),
|
|
400
|
+
request_id=request_id,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
response_file.write_text(json.dumps(response_data, indent=2, default=str))
|
|
404
|
+
|
|
405
|
+
def _get_body_preview(self, body: bytes) -> str:
|
|
406
|
+
"""Extract readable preview from body bytes.
|
|
407
|
+
|
|
408
|
+
- Decodes UTF-8 with error replacement
|
|
409
|
+
- Truncates to max_length
|
|
410
|
+
- Returns '<binary data>' for non-text content
|
|
411
|
+
"""
|
|
412
|
+
max_length = self.truncate_body_preview
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
text = body.decode("utf-8", errors="replace")
|
|
416
|
+
|
|
417
|
+
# Try to parse as JSON for better formatting
|
|
418
|
+
try:
|
|
419
|
+
json_data = json.loads(text)
|
|
420
|
+
formatted = json.dumps(json_data, indent=2)
|
|
421
|
+
if len(formatted) > max_length:
|
|
422
|
+
return formatted[:max_length] + "..."
|
|
423
|
+
return formatted
|
|
424
|
+
except json.JSONDecodeError:
|
|
425
|
+
# Not JSON, return as plain text
|
|
426
|
+
if len(text) > max_length:
|
|
427
|
+
return text[:max_length] + "..."
|
|
428
|
+
return text
|
|
429
|
+
except UnicodeDecodeError:
|
|
430
|
+
return "<binary data>"
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logger.debug("text_formatting_unexpected_error", error=str(e))
|
|
433
|
+
return "<binary data>"
|
|
434
|
+
|
|
435
|
+
# Streaming methods
|
|
436
|
+
async def log_stream_chunk(
|
|
437
|
+
self, request_id: str, chunk: bytes, chunk_number: int
|
|
438
|
+
) -> None:
|
|
439
|
+
"""Record individual stream chunk (optional, for deep debugging)."""
|
|
440
|
+
logger.debug(
|
|
441
|
+
"stream_chunk",
|
|
442
|
+
category="streaming",
|
|
443
|
+
request_id=request_id,
|
|
444
|
+
chunk_number=chunk_number,
|
|
445
|
+
chunk_size=len(chunk),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
async def log_error(
|
|
449
|
+
self,
|
|
450
|
+
request_id: str,
|
|
451
|
+
error: Exception | None,
|
|
452
|
+
duration: float | None = None,
|
|
453
|
+
provider: str | None = None,
|
|
454
|
+
) -> None:
|
|
455
|
+
"""Log error information."""
|
|
456
|
+
if not self.verbose_api:
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
error_data: dict[str, Any] = {
|
|
460
|
+
"request_id": request_id,
|
|
461
|
+
"error": str(error) if error else "unknown",
|
|
462
|
+
"category": "error",
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if duration is not None:
|
|
466
|
+
error_data["duration"] = duration
|
|
467
|
+
if provider:
|
|
468
|
+
error_data["provider"] = provider
|
|
469
|
+
|
|
470
|
+
logger.error("request_error", **error_data)
|
|
471
|
+
|
|
472
|
+
# Legacy compatibility methods
|
|
473
|
+
async def log_provider_request(
|
|
474
|
+
self,
|
|
475
|
+
request_id: str,
|
|
476
|
+
provider: str,
|
|
477
|
+
method: str,
|
|
478
|
+
url: str,
|
|
479
|
+
headers: dict[str, str],
|
|
480
|
+
body: bytes | None,
|
|
481
|
+
) -> None:
|
|
482
|
+
"""Log provider request."""
|
|
483
|
+
await self.log_request(
|
|
484
|
+
request_id=request_id,
|
|
485
|
+
method=method,
|
|
486
|
+
url=url,
|
|
487
|
+
headers=headers,
|
|
488
|
+
body=body,
|
|
489
|
+
request_type="provider",
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
async def log_provider_response(
|
|
493
|
+
self,
|
|
494
|
+
request_id: str,
|
|
495
|
+
provider: str,
|
|
496
|
+
status_code: int,
|
|
497
|
+
headers: dict[str, str],
|
|
498
|
+
body: bytes | None,
|
|
499
|
+
) -> None:
|
|
500
|
+
"""Log provider response."""
|
|
501
|
+
await self.log_response(
|
|
502
|
+
request_id=request_id,
|
|
503
|
+
status=status_code,
|
|
504
|
+
headers=headers,
|
|
505
|
+
body=body or b"",
|
|
506
|
+
response_type="provider",
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
async def log_stream_start(
|
|
510
|
+
self,
|
|
511
|
+
request_id: str,
|
|
512
|
+
provider: str | None = None,
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Log stream start."""
|
|
515
|
+
if not self.verbose_api:
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
log_data: dict[str, Any] = {
|
|
519
|
+
"request_id": request_id,
|
|
520
|
+
"category": "streaming",
|
|
521
|
+
}
|
|
522
|
+
if provider:
|
|
523
|
+
log_data["provider"] = provider
|
|
524
|
+
|
|
525
|
+
logger.info("stream_start", **log_data)
|
|
526
|
+
|
|
527
|
+
async def log_stream_complete(
|
|
528
|
+
self,
|
|
529
|
+
request_id: str,
|
|
530
|
+
provider: str | None = None,
|
|
531
|
+
total_chunks: int | None = None,
|
|
532
|
+
total_bytes: int | None = None,
|
|
533
|
+
usage_metrics: dict[str, Any] | None = None,
|
|
534
|
+
) -> None:
|
|
535
|
+
"""Log stream completion with metrics."""
|
|
536
|
+
if not self.verbose_api:
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
log_data: dict[str, Any] = {
|
|
540
|
+
"request_id": request_id,
|
|
541
|
+
"category": "streaming",
|
|
542
|
+
}
|
|
543
|
+
if provider:
|
|
544
|
+
log_data["provider"] = provider
|
|
545
|
+
if total_chunks is not None:
|
|
546
|
+
log_data["total_chunks"] = total_chunks
|
|
547
|
+
if total_bytes is not None:
|
|
548
|
+
log_data["total_bytes"] = total_bytes
|
|
549
|
+
if usage_metrics:
|
|
550
|
+
log_data["usage_metrics"] = usage_metrics
|
|
551
|
+
|
|
552
|
+
logger.info("stream_complete", **log_data)
|