ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
"""Access logging middleware for structured HTTP request/response logging."""
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import structlog
|
|
7
|
-
from fastapi import Request, Response
|
|
8
|
-
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
|
-
from starlette.types import ASGIApp
|
|
10
|
-
|
|
11
|
-
from ccproxy.api.dependencies import get_cached_settings
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
logger = structlog.get_logger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class AccessLogMiddleware(BaseHTTPMiddleware):
|
|
18
|
-
"""Middleware for structured access logging with request/response details."""
|
|
19
|
-
|
|
20
|
-
def __init__(self, app: ASGIApp):
|
|
21
|
-
"""Initialize the access log middleware.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
app: The ASGI application
|
|
25
|
-
"""
|
|
26
|
-
super().__init__(app)
|
|
27
|
-
|
|
28
|
-
async def dispatch(self, request: Request, call_next: Any) -> Response:
|
|
29
|
-
"""Process the request and log access details.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
request: The incoming HTTP request
|
|
33
|
-
call_next: The next middleware/handler in the chain
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
The HTTP response
|
|
37
|
-
"""
|
|
38
|
-
# Record start time
|
|
39
|
-
start_time = time.perf_counter()
|
|
40
|
-
|
|
41
|
-
# Store log storage in request state if collection is enabled
|
|
42
|
-
|
|
43
|
-
settings = get_cached_settings(request)
|
|
44
|
-
|
|
45
|
-
if settings.observability.logs_collection_enabled and hasattr(
|
|
46
|
-
request.app.state, "log_storage"
|
|
47
|
-
):
|
|
48
|
-
request.state.log_storage = request.app.state.log_storage
|
|
49
|
-
|
|
50
|
-
# Extract client info
|
|
51
|
-
client_ip = "unknown"
|
|
52
|
-
if request.client:
|
|
53
|
-
client_ip = request.client.host
|
|
54
|
-
|
|
55
|
-
# Extract request info
|
|
56
|
-
method = request.method
|
|
57
|
-
path = str(request.url.path)
|
|
58
|
-
query = str(request.url.query) if request.url.query else None
|
|
59
|
-
user_agent = request.headers.get("user-agent", "unknown")
|
|
60
|
-
|
|
61
|
-
# Get request ID from context if available
|
|
62
|
-
request_id: str | None = None
|
|
63
|
-
try:
|
|
64
|
-
if hasattr(request.state, "request_id"):
|
|
65
|
-
request_id = request.state.request_id
|
|
66
|
-
elif hasattr(request.state, "context"):
|
|
67
|
-
# Try to check if it's a RequestContext without importing
|
|
68
|
-
context = request.state.context
|
|
69
|
-
if hasattr(context, "request_id") and hasattr(context, "metadata"):
|
|
70
|
-
request_id = context.request_id
|
|
71
|
-
except Exception:
|
|
72
|
-
# Ignore any errors getting request_id
|
|
73
|
-
pass
|
|
74
|
-
|
|
75
|
-
# Process the request
|
|
76
|
-
response: Response | None = None
|
|
77
|
-
error_message: str | None = None
|
|
78
|
-
|
|
79
|
-
try:
|
|
80
|
-
response = await call_next(request)
|
|
81
|
-
except Exception as e:
|
|
82
|
-
# Capture error for logging
|
|
83
|
-
error_message = str(e)
|
|
84
|
-
# Re-raise to let error handlers process it
|
|
85
|
-
raise
|
|
86
|
-
finally:
|
|
87
|
-
try:
|
|
88
|
-
# Calculate duration
|
|
89
|
-
duration_seconds = time.perf_counter() - start_time
|
|
90
|
-
duration_ms = duration_seconds * 1000
|
|
91
|
-
|
|
92
|
-
# Extract response info
|
|
93
|
-
if response:
|
|
94
|
-
status_code = response.status_code
|
|
95
|
-
|
|
96
|
-
# Extract rate limit headers if present
|
|
97
|
-
rate_limit_info = {}
|
|
98
|
-
anthropic_request_id = None
|
|
99
|
-
for header_name, header_value in response.headers.items():
|
|
100
|
-
header_lower = header_name.lower()
|
|
101
|
-
# Capture x-ratelimit-* headers
|
|
102
|
-
if header_lower.startswith(
|
|
103
|
-
"x-ratelimit-"
|
|
104
|
-
) or header_lower.startswith("anthropic-ratelimit-"):
|
|
105
|
-
rate_limit_info[header_lower] = header_value
|
|
106
|
-
# Capture request-id from Anthropic's response
|
|
107
|
-
elif header_lower == "request-id":
|
|
108
|
-
anthropic_request_id = header_value
|
|
109
|
-
|
|
110
|
-
# Add anthropic request ID if present
|
|
111
|
-
if anthropic_request_id:
|
|
112
|
-
rate_limit_info["anthropic_request_id"] = anthropic_request_id
|
|
113
|
-
|
|
114
|
-
headers = request.state.context.metadata.get("headers", {})
|
|
115
|
-
headers.update(rate_limit_info)
|
|
116
|
-
request.state.context.metadata["headers"] = headers
|
|
117
|
-
request.state.context.metadata["status_code"] = status_code
|
|
118
|
-
# Extract metadata from context if available
|
|
119
|
-
context_metadata = {}
|
|
120
|
-
try:
|
|
121
|
-
if hasattr(request.state, "context"):
|
|
122
|
-
context = request.state.context
|
|
123
|
-
# Check if it has the expected attributes of RequestContext
|
|
124
|
-
if hasattr(context, "metadata") and isinstance(
|
|
125
|
-
context.metadata, dict
|
|
126
|
-
):
|
|
127
|
-
# Get all metadata from the context
|
|
128
|
-
context_metadata = context.metadata.copy()
|
|
129
|
-
# Remove fields we're already logging separately
|
|
130
|
-
for key in [
|
|
131
|
-
"method",
|
|
132
|
-
"path",
|
|
133
|
-
"client_ip",
|
|
134
|
-
"status_code",
|
|
135
|
-
"request_id",
|
|
136
|
-
"duration_ms",
|
|
137
|
-
"duration_seconds",
|
|
138
|
-
"query",
|
|
139
|
-
"user_agent",
|
|
140
|
-
"error_message",
|
|
141
|
-
]:
|
|
142
|
-
context_metadata.pop(key, None)
|
|
143
|
-
except Exception:
|
|
144
|
-
# Ignore any errors extracting context metadata
|
|
145
|
-
pass
|
|
146
|
-
|
|
147
|
-
# Use start-only logging - let context handle comprehensive access logging
|
|
148
|
-
# Only log basic request start info since context will handle complete access log
|
|
149
|
-
from ccproxy.observability.access_logger import log_request_start
|
|
150
|
-
|
|
151
|
-
log_request_start(
|
|
152
|
-
request_id=request_id or "unknown",
|
|
153
|
-
method=method,
|
|
154
|
-
path=path,
|
|
155
|
-
client_ip=client_ip,
|
|
156
|
-
user_agent=user_agent,
|
|
157
|
-
query=query,
|
|
158
|
-
**rate_limit_info,
|
|
159
|
-
)
|
|
160
|
-
else:
|
|
161
|
-
# Log error case
|
|
162
|
-
logger.error(
|
|
163
|
-
"access_log_error",
|
|
164
|
-
request_id=request_id,
|
|
165
|
-
method=method,
|
|
166
|
-
path=path,
|
|
167
|
-
query=query,
|
|
168
|
-
client_ip=client_ip,
|
|
169
|
-
user_agent=user_agent,
|
|
170
|
-
duration_ms=duration_ms,
|
|
171
|
-
duration_seconds=duration_seconds,
|
|
172
|
-
error_message=error_message or "No response generated",
|
|
173
|
-
exc_info=True,
|
|
174
|
-
)
|
|
175
|
-
except Exception as log_error:
|
|
176
|
-
# If logging fails, don't crash the app
|
|
177
|
-
# Use print as a last resort to indicate the issue
|
|
178
|
-
print(f"Failed to write access log: {log_error}")
|
|
179
|
-
|
|
180
|
-
return response
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
"""Request content logging middleware for capturing full HTTP request/response data."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from collections.abc import AsyncGenerator
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import structlog
|
|
8
|
-
from fastapi import Request, Response
|
|
9
|
-
from fastapi.responses import StreamingResponse
|
|
10
|
-
from starlette.middleware.base import BaseHTTPMiddleware
|
|
11
|
-
from starlette.types import ASGIApp
|
|
12
|
-
|
|
13
|
-
from ccproxy.utils.simple_request_logger import (
|
|
14
|
-
append_streaming_log,
|
|
15
|
-
write_request_log,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
logger = structlog.get_logger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class RequestContentLoggingMiddleware(BaseHTTPMiddleware):
|
|
23
|
-
"""Middleware for logging full HTTP request and response content."""
|
|
24
|
-
|
|
25
|
-
def __init__(self, app: ASGIApp):
|
|
26
|
-
"""Initialize the request content logging middleware.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
app: The ASGI application
|
|
30
|
-
"""
|
|
31
|
-
super().__init__(app)
|
|
32
|
-
|
|
33
|
-
async def dispatch(self, request: Request, call_next: Any) -> Any:
|
|
34
|
-
"""Process the request and log content.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
request: The incoming HTTP request
|
|
38
|
-
call_next: The next middleware/handler in the chain
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
The HTTP response
|
|
42
|
-
"""
|
|
43
|
-
# Get request ID and timestamp from context if available
|
|
44
|
-
request_id = self._get_request_id(request)
|
|
45
|
-
timestamp = self._get_timestamp_prefix(request)
|
|
46
|
-
|
|
47
|
-
# Log incoming request
|
|
48
|
-
await self._log_request(request, request_id, timestamp)
|
|
49
|
-
|
|
50
|
-
# Process the request
|
|
51
|
-
response = await call_next(request)
|
|
52
|
-
|
|
53
|
-
# Log outgoing response
|
|
54
|
-
await self._log_response(response, request_id, timestamp)
|
|
55
|
-
|
|
56
|
-
return response
|
|
57
|
-
|
|
58
|
-
def _get_request_id(self, request: Request) -> str:
|
|
59
|
-
"""Extract request ID from request state or context.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
request: The HTTP request
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
Request ID string or 'unknown' if not found
|
|
66
|
-
"""
|
|
67
|
-
try:
|
|
68
|
-
# Try to get from request state
|
|
69
|
-
if hasattr(request.state, "request_id"):
|
|
70
|
-
return str(request.state.request_id)
|
|
71
|
-
|
|
72
|
-
# Try to get from request context
|
|
73
|
-
if hasattr(request.state, "context"):
|
|
74
|
-
context = request.state.context
|
|
75
|
-
if hasattr(context, "request_id"):
|
|
76
|
-
return str(context.request_id)
|
|
77
|
-
|
|
78
|
-
# Fallback to UUID if available in headers
|
|
79
|
-
if "x-request-id" in request.headers:
|
|
80
|
-
return request.headers["x-request-id"]
|
|
81
|
-
|
|
82
|
-
except Exception:
|
|
83
|
-
pass # Ignore errors and use fallback
|
|
84
|
-
|
|
85
|
-
return "unknown"
|
|
86
|
-
|
|
87
|
-
def _get_timestamp_prefix(self, request: Request) -> str | None:
|
|
88
|
-
"""Extract timestamp prefix from request context.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
request: The HTTP request
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
Timestamp prefix string or None if not found
|
|
95
|
-
"""
|
|
96
|
-
try:
|
|
97
|
-
# Try to get from request context
|
|
98
|
-
if hasattr(request.state, "context"):
|
|
99
|
-
context = request.state.context
|
|
100
|
-
if hasattr(context, "get_log_timestamp_prefix"):
|
|
101
|
-
result = context.get_log_timestamp_prefix()
|
|
102
|
-
return str(result) if result is not None else None
|
|
103
|
-
except Exception:
|
|
104
|
-
pass # Ignore errors and use fallback
|
|
105
|
-
|
|
106
|
-
return None
|
|
107
|
-
|
|
108
|
-
async def _log_request(
|
|
109
|
-
self, request: Request, request_id: str, timestamp: str | None
|
|
110
|
-
) -> None:
|
|
111
|
-
"""Log incoming HTTP request content.
|
|
112
|
-
|
|
113
|
-
Args:
|
|
114
|
-
request: The HTTP request
|
|
115
|
-
request_id: Request identifier
|
|
116
|
-
timestamp: Timestamp prefix for the log file
|
|
117
|
-
"""
|
|
118
|
-
try:
|
|
119
|
-
# Read request body
|
|
120
|
-
body = await request.body()
|
|
121
|
-
|
|
122
|
-
# Create request log data
|
|
123
|
-
request_data = {
|
|
124
|
-
"method": request.method,
|
|
125
|
-
"url": str(request.url),
|
|
126
|
-
"headers": dict(request.headers),
|
|
127
|
-
"query_params": dict(request.query_params),
|
|
128
|
-
"path_params": dict(request.path_params)
|
|
129
|
-
if hasattr(request, "path_params")
|
|
130
|
-
else {},
|
|
131
|
-
"body_size": len(body) if body else 0,
|
|
132
|
-
"body": None,
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
# Try to parse body as JSON, fallback to string
|
|
136
|
-
if body:
|
|
137
|
-
try:
|
|
138
|
-
request_data["body"] = json.loads(body.decode("utf-8"))
|
|
139
|
-
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
140
|
-
try:
|
|
141
|
-
request_data["body"] = body.decode("utf-8", errors="replace")
|
|
142
|
-
except Exception:
|
|
143
|
-
request_data["body"] = f"<binary data of length {len(body)}>"
|
|
144
|
-
|
|
145
|
-
await write_request_log(
|
|
146
|
-
request_id=request_id,
|
|
147
|
-
log_type="middleware_request",
|
|
148
|
-
data=request_data,
|
|
149
|
-
timestamp=timestamp,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
except Exception as e:
|
|
153
|
-
logger.error(
|
|
154
|
-
"failed_to_log_request_content",
|
|
155
|
-
request_id=request_id,
|
|
156
|
-
error=str(e),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
async def _log_response(
|
|
160
|
-
self, response: Response, request_id: str, timestamp: str | None
|
|
161
|
-
) -> None:
|
|
162
|
-
"""Log outgoing HTTP response content.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
response: The HTTP response
|
|
166
|
-
request_id: Request identifier
|
|
167
|
-
timestamp: Timestamp prefix for the log file
|
|
168
|
-
"""
|
|
169
|
-
try:
|
|
170
|
-
if isinstance(response, StreamingResponse):
|
|
171
|
-
# Handle streaming response
|
|
172
|
-
await self._log_streaming_response(response, request_id, timestamp)
|
|
173
|
-
else:
|
|
174
|
-
# Handle regular response
|
|
175
|
-
await self._log_regular_response(response, request_id, timestamp)
|
|
176
|
-
|
|
177
|
-
except Exception as e:
|
|
178
|
-
logger.error(
|
|
179
|
-
"failed_to_log_response_content",
|
|
180
|
-
request_id=request_id,
|
|
181
|
-
error=str(e),
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
async def _log_regular_response(
|
|
185
|
-
self, response: Response, request_id: str, timestamp: str | None
|
|
186
|
-
) -> None:
|
|
187
|
-
"""Log regular (non-streaming) HTTP response.
|
|
188
|
-
|
|
189
|
-
Args:
|
|
190
|
-
response: The HTTP response
|
|
191
|
-
request_id: Request identifier
|
|
192
|
-
timestamp: Timestamp prefix for the log file
|
|
193
|
-
"""
|
|
194
|
-
# Create response log data
|
|
195
|
-
response_data = {
|
|
196
|
-
"status_code": response.status_code,
|
|
197
|
-
"headers": dict(response.headers),
|
|
198
|
-
"body": None,
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
# Try to get response body
|
|
202
|
-
if hasattr(response, "body") and response.body:
|
|
203
|
-
body = response.body
|
|
204
|
-
response_data["body_size"] = len(body)
|
|
205
|
-
|
|
206
|
-
try:
|
|
207
|
-
# Convert to bytes if needed
|
|
208
|
-
body_bytes = bytes(body) if isinstance(body, memoryview) else body
|
|
209
|
-
# Try to parse as JSON
|
|
210
|
-
response_data["body"] = json.loads(body_bytes.decode("utf-8"))
|
|
211
|
-
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
212
|
-
try:
|
|
213
|
-
# Fallback to string
|
|
214
|
-
body_bytes = bytes(body) if isinstance(body, memoryview) else body
|
|
215
|
-
response_data["body"] = body_bytes.decode("utf-8", errors="replace")
|
|
216
|
-
except Exception:
|
|
217
|
-
response_data["body"] = f"<binary data of length {len(body)}>"
|
|
218
|
-
else:
|
|
219
|
-
response_data["body_size"] = 0
|
|
220
|
-
|
|
221
|
-
await write_request_log(
|
|
222
|
-
request_id=request_id,
|
|
223
|
-
log_type="middleware_response",
|
|
224
|
-
data=response_data,
|
|
225
|
-
timestamp=timestamp,
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
async def _log_streaming_response(
|
|
229
|
-
self, response: StreamingResponse, request_id: str, timestamp: str | None
|
|
230
|
-
) -> None:
|
|
231
|
-
"""Log streaming HTTP response by wrapping the stream.
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
response: The streaming HTTP response
|
|
235
|
-
request_id: Request identifier
|
|
236
|
-
timestamp: Timestamp prefix for the log file
|
|
237
|
-
"""
|
|
238
|
-
# Log response metadata first
|
|
239
|
-
response_data = {
|
|
240
|
-
"status_code": response.status_code,
|
|
241
|
-
"headers": dict(response.headers),
|
|
242
|
-
"body_type": "streaming",
|
|
243
|
-
"media_type": response.media_type,
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
await write_request_log(
|
|
247
|
-
request_id=request_id,
|
|
248
|
-
log_type="middleware_response",
|
|
249
|
-
data=response_data,
|
|
250
|
-
timestamp=timestamp,
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
# Wrap the streaming response to capture content
|
|
254
|
-
original_body_iterator = response.body_iterator
|
|
255
|
-
|
|
256
|
-
def create_logged_body_iterator() -> AsyncGenerator[
|
|
257
|
-
str | bytes | memoryview[int], None
|
|
258
|
-
]:
|
|
259
|
-
"""Create wrapper around the original body iterator to log streaming content."""
|
|
260
|
-
|
|
261
|
-
async def logged_body_iterator() -> AsyncGenerator[
|
|
262
|
-
str | bytes | memoryview[int], None
|
|
263
|
-
]:
|
|
264
|
-
try:
|
|
265
|
-
async for chunk in original_body_iterator:
|
|
266
|
-
# Log chunk as raw data
|
|
267
|
-
if isinstance(chunk, bytes | bytearray):
|
|
268
|
-
await append_streaming_log(
|
|
269
|
-
request_id=request_id,
|
|
270
|
-
log_type="middleware_streaming",
|
|
271
|
-
data=bytes(chunk),
|
|
272
|
-
timestamp=timestamp,
|
|
273
|
-
)
|
|
274
|
-
elif isinstance(chunk, str):
|
|
275
|
-
await append_streaming_log(
|
|
276
|
-
request_id=request_id,
|
|
277
|
-
log_type="middleware_streaming",
|
|
278
|
-
data=chunk.encode("utf-8"),
|
|
279
|
-
timestamp=timestamp,
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
yield chunk
|
|
283
|
-
|
|
284
|
-
except Exception as e:
|
|
285
|
-
logger.error(
|
|
286
|
-
"error_in_streaming_response_logging",
|
|
287
|
-
request_id=request_id,
|
|
288
|
-
error=str(e),
|
|
289
|
-
)
|
|
290
|
-
# Continue with original iterator if logging fails
|
|
291
|
-
async for chunk in original_body_iterator:
|
|
292
|
-
yield chunk
|
|
293
|
-
|
|
294
|
-
return logged_body_iterator()
|
|
295
|
-
|
|
296
|
-
# Replace the body iterator with our logged version
|
|
297
|
-
response.body_iterator = create_logged_body_iterator()
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""Server header middleware to set a default server header for non-proxy routes."""
|
|
2
|
-
|
|
3
|
-
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ServerHeaderMiddleware:
|
|
7
|
-
"""Middleware to set a default server header for responses.
|
|
8
|
-
|
|
9
|
-
This middleware adds a server header to responses that don't already have one.
|
|
10
|
-
Proxy responses using ProxyResponse will preserve their upstream server header,
|
|
11
|
-
while other routes will get the default header.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
def __init__(self, app: ASGIApp, server_name: str = "Claude Code Proxy"):
|
|
15
|
-
"""Initialize the server header middleware.
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
app: The ASGI application
|
|
19
|
-
server_name: The default server name to use
|
|
20
|
-
"""
|
|
21
|
-
self.app = app
|
|
22
|
-
self.server_name = server_name
|
|
23
|
-
|
|
24
|
-
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
25
|
-
"""ASGI application entrypoint."""
|
|
26
|
-
if scope["type"] != "http":
|
|
27
|
-
await self.app(scope, receive, send)
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
async def send_wrapper(message: Message) -> None:
|
|
31
|
-
if message["type"] == "http.response.start":
|
|
32
|
-
headers = list(message.get("headers", []))
|
|
33
|
-
|
|
34
|
-
# Check if server header already exists
|
|
35
|
-
has_server = any(header[0].lower() == b"server" for header in headers)
|
|
36
|
-
|
|
37
|
-
# Only add server header for non-proxy routes
|
|
38
|
-
# Proxy routes will have their server header preserved from upstream
|
|
39
|
-
if not has_server:
|
|
40
|
-
# Check if this looks like a proxy response by looking for specific headers
|
|
41
|
-
is_proxy_response = any(
|
|
42
|
-
header[0].lower()
|
|
43
|
-
in [
|
|
44
|
-
b"cf-ray",
|
|
45
|
-
b"cf-cache-status",
|
|
46
|
-
b"anthropic-ratelimit-unified-status",
|
|
47
|
-
]
|
|
48
|
-
for header in headers
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
# Only add our server header if this is NOT a proxy response
|
|
52
|
-
if not is_proxy_response:
|
|
53
|
-
headers.append((b"server", self.server_name.encode()))
|
|
54
|
-
message["headers"] = headers
|
|
55
|
-
|
|
56
|
-
await send(message)
|
|
57
|
-
|
|
58
|
-
await self.app(scope, receive, send_wrapper)
|
ccproxy/api/responses.py
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"""Custom response classes for preserving proxy headers."""
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from fastapi import Response
|
|
6
|
-
from starlette.types import Receive, Scope, Send
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class ProxyResponse(Response):
|
|
10
|
-
"""Custom response class that preserves all headers from upstream API.
|
|
11
|
-
|
|
12
|
-
This response class ensures that headers like 'server' from the upstream
|
|
13
|
-
API are preserved and not overridden by Uvicorn/Starlette.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
def __init__(
|
|
17
|
-
self,
|
|
18
|
-
content: Any = None,
|
|
19
|
-
status_code: int = 200,
|
|
20
|
-
headers: dict[str, str] | None = None,
|
|
21
|
-
media_type: str | None = None,
|
|
22
|
-
background: Any = None,
|
|
23
|
-
):
|
|
24
|
-
"""Initialize the proxy response with preserved headers.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
content: Response content
|
|
28
|
-
status_code: HTTP status code
|
|
29
|
-
headers: Headers to preserve from upstream
|
|
30
|
-
media_type: Content type
|
|
31
|
-
background: Background task
|
|
32
|
-
"""
|
|
33
|
-
super().__init__(
|
|
34
|
-
content=content,
|
|
35
|
-
status_code=status_code,
|
|
36
|
-
headers=headers,
|
|
37
|
-
media_type=media_type,
|
|
38
|
-
background=background,
|
|
39
|
-
)
|
|
40
|
-
# Store original headers for preservation
|
|
41
|
-
self._preserve_headers = headers or {}
|
|
42
|
-
|
|
43
|
-
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
44
|
-
"""Override the ASGI call to ensure headers are preserved.
|
|
45
|
-
|
|
46
|
-
This method intercepts the response sending process to ensure
|
|
47
|
-
that our headers are not overridden by the server.
|
|
48
|
-
"""
|
|
49
|
-
# Ensure we include all original headers, including 'server'
|
|
50
|
-
headers_list = []
|
|
51
|
-
seen_headers = set()
|
|
52
|
-
|
|
53
|
-
# Add all headers from the response, but skip content-length
|
|
54
|
-
# as we'll recalculate it based on actual body
|
|
55
|
-
for name, value in self._preserve_headers.items():
|
|
56
|
-
lower_name = name.lower()
|
|
57
|
-
# Skip content-length and transfer-encoding as we'll set them correctly
|
|
58
|
-
if (
|
|
59
|
-
lower_name not in ["content-length", "transfer-encoding"]
|
|
60
|
-
and lower_name not in seen_headers
|
|
61
|
-
):
|
|
62
|
-
headers_list.append((lower_name.encode(), value.encode()))
|
|
63
|
-
seen_headers.add(lower_name)
|
|
64
|
-
|
|
65
|
-
# Always set correct content-length based on actual body
|
|
66
|
-
if self.body:
|
|
67
|
-
headers_list.append((b"content-length", str(len(self.body)).encode()))
|
|
68
|
-
else:
|
|
69
|
-
headers_list.append((b"content-length", b"0"))
|
|
70
|
-
|
|
71
|
-
# Ensure we have content-type
|
|
72
|
-
has_content_type = any(h[0] == b"content-type" for h in headers_list)
|
|
73
|
-
if not has_content_type and self.media_type:
|
|
74
|
-
headers_list.append((b"content-type", self.media_type.encode()))
|
|
75
|
-
|
|
76
|
-
await send(
|
|
77
|
-
{
|
|
78
|
-
"type": "http.response.start",
|
|
79
|
-
"status": self.status_code,
|
|
80
|
-
"headers": headers_list,
|
|
81
|
-
}
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
await send(
|
|
85
|
-
{
|
|
86
|
-
"type": "http.response.body",
|
|
87
|
-
"body": self.body,
|
|
88
|
-
}
|
|
89
|
-
)
|