ccproxy-api 0.1.7__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 +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.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.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 -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.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,812 +0,0 @@
|
|
|
1
|
-
"""HTTP-level transformers for proxy service."""
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
-
|
|
5
|
-
import structlog
|
|
6
|
-
from typing_extensions import TypedDict
|
|
7
|
-
|
|
8
|
-
from ccproxy.core.transformers import RequestTransformer, ResponseTransformer
|
|
9
|
-
from ccproxy.core.types import ProxyRequest, ProxyResponse, TransformContext
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
pass
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
logger = structlog.get_logger(__name__)
|
|
17
|
-
|
|
18
|
-
# Claude Code system prompt constants
|
|
19
|
-
claude_code_prompt = "You are Claude Code, Anthropic's official CLI for Claude."
|
|
20
|
-
|
|
21
|
-
# claude_code_prompt = "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# important-instruction-reminders\nDo what has been asked; nothing more, nothing less.\nNEVER create files unless they're absolutely necessary for achieving your goal.\nALWAYS prefer editing an existing file to creating a new one.\nNEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n\n \n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</system-reminder>\n"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def get_detected_system_field(
|
|
25
|
-
app_state: Any = None, injection_mode: str = "minimal"
|
|
26
|
-
) -> Any:
|
|
27
|
-
"""Get the detected system field for injection.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
app_state: App state containing detection data
|
|
31
|
-
injection_mode: 'minimal' or 'full' mode
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
The system field to inject (preserving exact Claude CLI structure), or None if no detection data available
|
|
35
|
-
"""
|
|
36
|
-
if not app_state or not hasattr(app_state, "claude_detection_data"):
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
|
-
claude_data = app_state.claude_detection_data
|
|
40
|
-
detected_system = claude_data.system_prompt.system_field
|
|
41
|
-
|
|
42
|
-
if injection_mode == "full":
|
|
43
|
-
# Return the complete detected system field exactly as Claude CLI sent it
|
|
44
|
-
return detected_system
|
|
45
|
-
else:
|
|
46
|
-
# Minimal mode: extract just the first system message, preserving its structure
|
|
47
|
-
if isinstance(detected_system, str):
|
|
48
|
-
return detected_system
|
|
49
|
-
elif isinstance(detected_system, list) and detected_system:
|
|
50
|
-
# Return only the first message object with its complete structure (type, text, cache_control)
|
|
51
|
-
return [detected_system[0]]
|
|
52
|
-
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def get_fallback_system_field() -> list[dict[str, Any]]:
|
|
57
|
-
"""Get fallback system field when no detection data is available."""
|
|
58
|
-
return [
|
|
59
|
-
{
|
|
60
|
-
"type": "text",
|
|
61
|
-
"text": claude_code_prompt,
|
|
62
|
-
"cache_control": {"type": "ephemeral"},
|
|
63
|
-
}
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class RequestData(TypedDict):
|
|
68
|
-
"""Typed structure for transformed request data."""
|
|
69
|
-
|
|
70
|
-
method: str
|
|
71
|
-
url: str
|
|
72
|
-
headers: dict[str, str]
|
|
73
|
-
body: bytes | None
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class ResponseData(TypedDict):
|
|
77
|
-
"""Typed structure for transformed response data."""
|
|
78
|
-
|
|
79
|
-
status_code: int
|
|
80
|
-
headers: dict[str, str]
|
|
81
|
-
body: bytes
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class HTTPRequestTransformer(RequestTransformer):
|
|
85
|
-
"""HTTP request transformer that implements the abstract RequestTransformer interface."""
|
|
86
|
-
|
|
87
|
-
def __init__(self) -> None:
|
|
88
|
-
"""Initialize HTTP request transformer."""
|
|
89
|
-
super().__init__()
|
|
90
|
-
|
|
91
|
-
async def _transform_request(
|
|
92
|
-
self, request: ProxyRequest, context: TransformContext | None = None
|
|
93
|
-
) -> ProxyRequest:
|
|
94
|
-
"""Transform a proxy request according to the abstract interface.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
request: The structured proxy request to transform
|
|
98
|
-
context: Optional transformation context
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
The transformed proxy request
|
|
102
|
-
"""
|
|
103
|
-
# Transform path
|
|
104
|
-
transformed_path = self.transform_path(
|
|
105
|
-
request.url.split("?")[0].split("/", 3)[-1]
|
|
106
|
-
if "/" in request.url
|
|
107
|
-
else request.url
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
# Build new URL with transformed path
|
|
111
|
-
base_url = "https://api.anthropic.com"
|
|
112
|
-
new_url = f"{base_url}{transformed_path}"
|
|
113
|
-
|
|
114
|
-
# Add query parameters
|
|
115
|
-
if request.params:
|
|
116
|
-
import urllib.parse
|
|
117
|
-
|
|
118
|
-
query_string = urllib.parse.urlencode(request.params)
|
|
119
|
-
new_url = f"{new_url}?{query_string}"
|
|
120
|
-
|
|
121
|
-
# Transform headers (requires access token from context)
|
|
122
|
-
access_token = ""
|
|
123
|
-
if context and hasattr(context, "access_token"):
|
|
124
|
-
access_token = context.access_token
|
|
125
|
-
elif context and isinstance(context, dict):
|
|
126
|
-
access_token = context.get("access_token", "")
|
|
127
|
-
|
|
128
|
-
# Extract app_state from context if available
|
|
129
|
-
app_state = None
|
|
130
|
-
if context and hasattr(context, "app_state"):
|
|
131
|
-
app_state = context.app_state
|
|
132
|
-
elif context and isinstance(context, dict):
|
|
133
|
-
app_state = context.get("app_state")
|
|
134
|
-
|
|
135
|
-
transformed_headers = self.create_proxy_headers(
|
|
136
|
-
request.headers, access_token, self.proxy_mode, app_state
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
# Transform body
|
|
140
|
-
transformed_body = request.body
|
|
141
|
-
if request.body:
|
|
142
|
-
if isinstance(request.body, bytes):
|
|
143
|
-
transformed_body = self.transform_request_body(
|
|
144
|
-
request.body, transformed_path, self.proxy_mode, app_state
|
|
145
|
-
)
|
|
146
|
-
elif isinstance(request.body, str):
|
|
147
|
-
transformed_body = self.transform_request_body(
|
|
148
|
-
request.body.encode("utf-8"),
|
|
149
|
-
transformed_path,
|
|
150
|
-
self.proxy_mode,
|
|
151
|
-
app_state,
|
|
152
|
-
)
|
|
153
|
-
elif isinstance(request.body, dict):
|
|
154
|
-
import json
|
|
155
|
-
|
|
156
|
-
transformed_body = self.transform_request_body(
|
|
157
|
-
json.dumps(request.body).encode("utf-8"),
|
|
158
|
-
transformed_path,
|
|
159
|
-
self.proxy_mode,
|
|
160
|
-
app_state,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
# Create new transformed request
|
|
164
|
-
return ProxyRequest(
|
|
165
|
-
method=request.method,
|
|
166
|
-
url=new_url,
|
|
167
|
-
headers=transformed_headers,
|
|
168
|
-
params={}, # Already included in URL
|
|
169
|
-
body=transformed_body,
|
|
170
|
-
protocol=request.protocol,
|
|
171
|
-
timeout=request.timeout,
|
|
172
|
-
metadata=request.metadata,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
async def transform_proxy_request(
|
|
176
|
-
self,
|
|
177
|
-
method: str,
|
|
178
|
-
path: str,
|
|
179
|
-
headers: dict[str, str],
|
|
180
|
-
body: bytes | None,
|
|
181
|
-
query_params: dict[str, str | list[str]] | None,
|
|
182
|
-
access_token: str,
|
|
183
|
-
target_base_url: str = "https://api.anthropic.com",
|
|
184
|
-
app_state: Any = None,
|
|
185
|
-
injection_mode: str = "minimal",
|
|
186
|
-
) -> RequestData:
|
|
187
|
-
"""Transform request using direct parameters from ProxyService.
|
|
188
|
-
|
|
189
|
-
This method provides the same functionality as ProxyService._transform_request()
|
|
190
|
-
but is properly located in the transformer layer.
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
method: HTTP method
|
|
194
|
-
path: Request path
|
|
195
|
-
headers: Request headers
|
|
196
|
-
body: Request body
|
|
197
|
-
query_params: Query parameters
|
|
198
|
-
access_token: OAuth access token
|
|
199
|
-
target_base_url: Base URL for the target API
|
|
200
|
-
app_state: Optional app state containing detection data
|
|
201
|
-
injection_mode: System prompt injection mode
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
Dictionary with transformed request data (method, url, headers, body)
|
|
205
|
-
"""
|
|
206
|
-
import urllib.parse
|
|
207
|
-
|
|
208
|
-
# Transform path
|
|
209
|
-
transformed_path = self.transform_path(path, self.proxy_mode)
|
|
210
|
-
target_url = f"{target_base_url.rstrip('/')}{transformed_path}"
|
|
211
|
-
|
|
212
|
-
# Add beta=true query parameter for /v1/messages requests if not already present
|
|
213
|
-
if transformed_path == "/v1/messages":
|
|
214
|
-
if query_params is None:
|
|
215
|
-
query_params = {}
|
|
216
|
-
elif "beta" not in query_params:
|
|
217
|
-
query_params = dict(query_params) # Make a copy
|
|
218
|
-
|
|
219
|
-
if "beta" not in query_params:
|
|
220
|
-
query_params["beta"] = "true"
|
|
221
|
-
|
|
222
|
-
# Transform body first (as it might change size)
|
|
223
|
-
proxy_body = None
|
|
224
|
-
if body:
|
|
225
|
-
proxy_body = self.transform_request_body(
|
|
226
|
-
body, path, self.proxy_mode, app_state, injection_mode
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
# Transform headers (and update Content-Length if body changed)
|
|
230
|
-
proxy_headers = self.create_proxy_headers(
|
|
231
|
-
headers, access_token, self.proxy_mode, app_state
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
# Update Content-Length if body was transformed and size changed
|
|
235
|
-
if proxy_body and body and len(proxy_body) != len(body):
|
|
236
|
-
# Remove any existing content-length headers (case-insensitive)
|
|
237
|
-
proxy_headers = {
|
|
238
|
-
k: v for k, v in proxy_headers.items() if k.lower() != "content-length"
|
|
239
|
-
}
|
|
240
|
-
proxy_headers["Content-Length"] = str(len(proxy_body))
|
|
241
|
-
elif proxy_body and not body:
|
|
242
|
-
# New body was created where none existed
|
|
243
|
-
proxy_headers["Content-Length"] = str(len(proxy_body))
|
|
244
|
-
|
|
245
|
-
# Add query parameters to URL if present
|
|
246
|
-
if query_params:
|
|
247
|
-
query_string = urllib.parse.urlencode(query_params)
|
|
248
|
-
target_url = f"{target_url}?{query_string}"
|
|
249
|
-
|
|
250
|
-
return RequestData(
|
|
251
|
-
method=method,
|
|
252
|
-
url=target_url,
|
|
253
|
-
headers=proxy_headers,
|
|
254
|
-
body=proxy_body,
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
def transform_path(self, path: str, proxy_mode: str = "full") -> str:
|
|
258
|
-
"""Transform request path."""
|
|
259
|
-
# Remove /api prefix if present (for new proxy endpoints)
|
|
260
|
-
if path.startswith("/api"):
|
|
261
|
-
path = path[4:] # Remove "/api" prefix
|
|
262
|
-
|
|
263
|
-
# Remove /openai prefix if present
|
|
264
|
-
if path.startswith("/openai"):
|
|
265
|
-
path = path[7:] # Remove "/openai" prefix
|
|
266
|
-
|
|
267
|
-
# Convert OpenAI chat completions to Anthropic messages
|
|
268
|
-
if path == "/v1/chat/completions":
|
|
269
|
-
return "/v1/messages"
|
|
270
|
-
|
|
271
|
-
return path
|
|
272
|
-
|
|
273
|
-
def create_proxy_headers(
|
|
274
|
-
self,
|
|
275
|
-
headers: dict[str, str],
|
|
276
|
-
access_token: str,
|
|
277
|
-
proxy_mode: str = "full",
|
|
278
|
-
app_state: Any = None,
|
|
279
|
-
) -> dict[str, str]:
|
|
280
|
-
"""Create proxy headers from original headers with Claude CLI identity."""
|
|
281
|
-
proxy_headers = {}
|
|
282
|
-
|
|
283
|
-
# Strip potentially problematic headers
|
|
284
|
-
excluded_headers = {
|
|
285
|
-
"host",
|
|
286
|
-
"x-forwarded-for",
|
|
287
|
-
"x-forwarded-proto",
|
|
288
|
-
"x-forwarded-host",
|
|
289
|
-
"forwarded",
|
|
290
|
-
# Authentication headers to be replaced
|
|
291
|
-
"authorization",
|
|
292
|
-
"x-api-key",
|
|
293
|
-
# Compression headers to avoid decompression issues
|
|
294
|
-
"accept-encoding",
|
|
295
|
-
"content-encoding",
|
|
296
|
-
# CORS headers - should not be forwarded to upstream
|
|
297
|
-
"origin",
|
|
298
|
-
"access-control-request-method",
|
|
299
|
-
"access-control-request-headers",
|
|
300
|
-
"access-control-allow-origin",
|
|
301
|
-
"access-control-allow-methods",
|
|
302
|
-
"access-control-allow-headers",
|
|
303
|
-
"access-control-allow-credentials",
|
|
304
|
-
"access-control-max-age",
|
|
305
|
-
"access-control-expose-headers",
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
# Copy important headers (excluding problematic ones)
|
|
309
|
-
for key, value in headers.items():
|
|
310
|
-
lower_key = key.lower()
|
|
311
|
-
if lower_key not in excluded_headers:
|
|
312
|
-
proxy_headers[key] = value
|
|
313
|
-
|
|
314
|
-
# Set authentication with OAuth token
|
|
315
|
-
if access_token:
|
|
316
|
-
proxy_headers["Authorization"] = f"Bearer {access_token}"
|
|
317
|
-
|
|
318
|
-
# Set defaults for essential headers
|
|
319
|
-
if "content-type" not in [k.lower() for k in proxy_headers]:
|
|
320
|
-
proxy_headers["Content-Type"] = "application/json"
|
|
321
|
-
if "accept" not in [k.lower() for k in proxy_headers]:
|
|
322
|
-
proxy_headers["Accept"] = "application/json"
|
|
323
|
-
if "connection" not in [k.lower() for k in proxy_headers]:
|
|
324
|
-
proxy_headers["Connection"] = "keep-alive"
|
|
325
|
-
|
|
326
|
-
# Use detected Claude CLI headers when available
|
|
327
|
-
if app_state and hasattr(app_state, "claude_detection_data"):
|
|
328
|
-
claude_data = app_state.claude_detection_data
|
|
329
|
-
detected_headers = claude_data.headers.to_headers_dict()
|
|
330
|
-
proxy_headers.update(detected_headers)
|
|
331
|
-
logger.debug("using_detected_headers", version=claude_data.claude_version)
|
|
332
|
-
else:
|
|
333
|
-
# Fallback to hardcoded Claude/Anthropic headers
|
|
334
|
-
proxy_headers["anthropic-beta"] = (
|
|
335
|
-
"claude-code-20250219,oauth-2025-04-20,"
|
|
336
|
-
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
|
337
|
-
)
|
|
338
|
-
proxy_headers["anthropic-version"] = "2023-06-01"
|
|
339
|
-
proxy_headers["anthropic-dangerous-direct-browser-access"] = "true"
|
|
340
|
-
|
|
341
|
-
# Claude CLI identity headers
|
|
342
|
-
proxy_headers["x-app"] = "cli"
|
|
343
|
-
proxy_headers["User-Agent"] = "claude-cli/1.0.60 (external, cli)"
|
|
344
|
-
|
|
345
|
-
# Stainless SDK compatibility headers
|
|
346
|
-
proxy_headers["X-Stainless-Lang"] = "js"
|
|
347
|
-
proxy_headers["X-Stainless-Retry-Count"] = "0"
|
|
348
|
-
proxy_headers["X-Stainless-Timeout"] = "60"
|
|
349
|
-
proxy_headers["X-Stainless-Package-Version"] = "0.55.1"
|
|
350
|
-
proxy_headers["X-Stainless-OS"] = "Linux"
|
|
351
|
-
proxy_headers["X-Stainless-Arch"] = "x64"
|
|
352
|
-
proxy_headers["X-Stainless-Runtime"] = "node"
|
|
353
|
-
proxy_headers["X-Stainless-Runtime-Version"] = "v24.3.0"
|
|
354
|
-
logger.debug("using_fallback_headers")
|
|
355
|
-
|
|
356
|
-
# Standard HTTP headers for proper API interaction
|
|
357
|
-
proxy_headers["accept-language"] = "*"
|
|
358
|
-
proxy_headers["sec-fetch-mode"] = "cors"
|
|
359
|
-
# Note: accept-encoding removed to avoid compression issues
|
|
360
|
-
# HTTPX handles compression automatically
|
|
361
|
-
|
|
362
|
-
return proxy_headers
|
|
363
|
-
|
|
364
|
-
def _count_cache_control_blocks(self, data: dict[str, Any]) -> dict[str, int]:
|
|
365
|
-
"""Count cache_control blocks in different parts of the request.
|
|
366
|
-
|
|
367
|
-
Returns:
|
|
368
|
-
Dictionary with counts for 'injected_system', 'user_system', and 'messages'
|
|
369
|
-
"""
|
|
370
|
-
counts = {"injected_system": 0, "user_system": 0, "messages": 0}
|
|
371
|
-
|
|
372
|
-
# Count in system field
|
|
373
|
-
system = data.get("system")
|
|
374
|
-
if system:
|
|
375
|
-
if isinstance(system, str):
|
|
376
|
-
# String system prompts don't have cache_control
|
|
377
|
-
pass
|
|
378
|
-
elif isinstance(system, list):
|
|
379
|
-
# Count cache_control in system prompt blocks
|
|
380
|
-
# The first block(s) are injected, rest are user's
|
|
381
|
-
injected_count = 0
|
|
382
|
-
for i, block in enumerate(system):
|
|
383
|
-
if isinstance(block, dict) and "cache_control" in block:
|
|
384
|
-
# Check if this is the injected prompt (contains Claude Code identity)
|
|
385
|
-
text = block.get("text", "")
|
|
386
|
-
if "Claude Code" in text or "Anthropic's official CLI" in text:
|
|
387
|
-
counts["injected_system"] += 1
|
|
388
|
-
injected_count = max(injected_count, i + 1)
|
|
389
|
-
elif i < injected_count:
|
|
390
|
-
# Part of injected system (multiple blocks)
|
|
391
|
-
counts["injected_system"] += 1
|
|
392
|
-
else:
|
|
393
|
-
counts["user_system"] += 1
|
|
394
|
-
|
|
395
|
-
# Count in messages
|
|
396
|
-
messages = data.get("messages", [])
|
|
397
|
-
for msg in messages:
|
|
398
|
-
content = msg.get("content")
|
|
399
|
-
if isinstance(content, list):
|
|
400
|
-
for block in content:
|
|
401
|
-
if isinstance(block, dict) and "cache_control" in block:
|
|
402
|
-
counts["messages"] += 1
|
|
403
|
-
|
|
404
|
-
return counts
|
|
405
|
-
|
|
406
|
-
def _limit_cache_control_blocks(
|
|
407
|
-
self, data: dict[str, Any], max_blocks: int = 4
|
|
408
|
-
) -> dict[str, Any]:
|
|
409
|
-
"""Limit the number of cache_control blocks to comply with Anthropic's limit.
|
|
410
|
-
|
|
411
|
-
Priority order:
|
|
412
|
-
1. Injected system prompt cache_control (highest priority - Claude Code identity)
|
|
413
|
-
2. User's system prompt cache_control
|
|
414
|
-
3. User's message cache_control (lowest priority)
|
|
415
|
-
|
|
416
|
-
Args:
|
|
417
|
-
data: Request data dictionary
|
|
418
|
-
max_blocks: Maximum number of cache_control blocks allowed (default: 4)
|
|
419
|
-
|
|
420
|
-
Returns:
|
|
421
|
-
Modified data dictionary with cache_control blocks limited
|
|
422
|
-
"""
|
|
423
|
-
import copy
|
|
424
|
-
|
|
425
|
-
# Deep copy to avoid modifying original
|
|
426
|
-
data = copy.deepcopy(data)
|
|
427
|
-
|
|
428
|
-
# Count existing blocks
|
|
429
|
-
counts = self._count_cache_control_blocks(data)
|
|
430
|
-
total = counts["injected_system"] + counts["user_system"] + counts["messages"]
|
|
431
|
-
|
|
432
|
-
if total <= max_blocks:
|
|
433
|
-
# No need to remove anything
|
|
434
|
-
return data
|
|
435
|
-
|
|
436
|
-
logger.warning(
|
|
437
|
-
"cache_control_limit_exceeded",
|
|
438
|
-
total_blocks=total,
|
|
439
|
-
max_blocks=max_blocks,
|
|
440
|
-
injected=counts["injected_system"],
|
|
441
|
-
user_system=counts["user_system"],
|
|
442
|
-
messages=counts["messages"],
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
# Calculate how many to remove
|
|
446
|
-
to_remove = total - max_blocks
|
|
447
|
-
removed = 0
|
|
448
|
-
|
|
449
|
-
# Remove from messages first (lowest priority)
|
|
450
|
-
if to_remove > 0 and counts["messages"] > 0:
|
|
451
|
-
messages = data.get("messages", [])
|
|
452
|
-
for msg in reversed(messages): # Remove from end first
|
|
453
|
-
if removed >= to_remove:
|
|
454
|
-
break
|
|
455
|
-
content = msg.get("content")
|
|
456
|
-
if isinstance(content, list):
|
|
457
|
-
for block in reversed(content):
|
|
458
|
-
if removed >= to_remove:
|
|
459
|
-
break
|
|
460
|
-
if isinstance(block, dict) and "cache_control" in block:
|
|
461
|
-
del block["cache_control"]
|
|
462
|
-
removed += 1
|
|
463
|
-
logger.debug("removed_cache_control", location="message")
|
|
464
|
-
|
|
465
|
-
# Remove from user system prompts next
|
|
466
|
-
if removed < to_remove and counts["user_system"] > 0:
|
|
467
|
-
system = data.get("system")
|
|
468
|
-
if isinstance(system, list):
|
|
469
|
-
# Find and remove cache_control from user system blocks (non-injected)
|
|
470
|
-
for block in reversed(system):
|
|
471
|
-
if removed >= to_remove:
|
|
472
|
-
break
|
|
473
|
-
if isinstance(block, dict) and "cache_control" in block:
|
|
474
|
-
text = block.get("text", "")
|
|
475
|
-
# Skip injected prompts (highest priority)
|
|
476
|
-
if (
|
|
477
|
-
"Claude Code" not in text
|
|
478
|
-
and "Anthropic's official CLI" not in text
|
|
479
|
-
):
|
|
480
|
-
del block["cache_control"]
|
|
481
|
-
removed += 1
|
|
482
|
-
logger.debug(
|
|
483
|
-
"removed_cache_control", location="user_system"
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
# In theory, we should never need to remove injected system cache_control
|
|
487
|
-
# but include this for completeness
|
|
488
|
-
if removed < to_remove:
|
|
489
|
-
logger.error(
|
|
490
|
-
"cannot_preserve_injected_cache_control",
|
|
491
|
-
needed_to_remove=to_remove,
|
|
492
|
-
actually_removed=removed,
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
return data
|
|
496
|
-
|
|
497
|
-
def transform_request_body(
|
|
498
|
-
self,
|
|
499
|
-
body: bytes,
|
|
500
|
-
path: str,
|
|
501
|
-
proxy_mode: str = "full",
|
|
502
|
-
app_state: Any = None,
|
|
503
|
-
injection_mode: str = "minimal",
|
|
504
|
-
) -> bytes:
|
|
505
|
-
"""Transform request body."""
|
|
506
|
-
if not body:
|
|
507
|
-
return body
|
|
508
|
-
|
|
509
|
-
# Check if this is an OpenAI request and transform it
|
|
510
|
-
if self._is_openai_request(path, body):
|
|
511
|
-
# Transform OpenAI format to Anthropic format
|
|
512
|
-
body = self._transform_openai_to_anthropic(body)
|
|
513
|
-
|
|
514
|
-
# Apply system prompt transformation for Claude Code identity
|
|
515
|
-
return self.transform_system_prompt(body, app_state, injection_mode)
|
|
516
|
-
|
|
517
|
-
def transform_system_prompt(
|
|
518
|
-
self, body: bytes, app_state: Any = None, injection_mode: str = "minimal"
|
|
519
|
-
) -> bytes:
|
|
520
|
-
"""Transform system prompt based on injection mode.
|
|
521
|
-
|
|
522
|
-
Args:
|
|
523
|
-
body: Original request body as bytes
|
|
524
|
-
app_state: Optional app state containing detection data
|
|
525
|
-
injection_mode: System prompt injection mode ('minimal' or 'full')
|
|
526
|
-
|
|
527
|
-
Returns:
|
|
528
|
-
Transformed request body as bytes with system prompt injection
|
|
529
|
-
"""
|
|
530
|
-
try:
|
|
531
|
-
import json
|
|
532
|
-
|
|
533
|
-
data = json.loads(body.decode("utf-8"))
|
|
534
|
-
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
535
|
-
# Return original if not valid JSON
|
|
536
|
-
logger.warning(
|
|
537
|
-
"http_transform_json_decode_failed",
|
|
538
|
-
error=str(e),
|
|
539
|
-
body_preview=body[:200].decode("utf-8", errors="replace")
|
|
540
|
-
if body
|
|
541
|
-
else None,
|
|
542
|
-
body_length=len(body) if body else 0,
|
|
543
|
-
)
|
|
544
|
-
return body
|
|
545
|
-
|
|
546
|
-
# Get the system field to inject
|
|
547
|
-
detected_system = get_detected_system_field(app_state, injection_mode)
|
|
548
|
-
if detected_system is None:
|
|
549
|
-
# No detection data, use fallback
|
|
550
|
-
detected_system = get_fallback_system_field()
|
|
551
|
-
|
|
552
|
-
# Always inject the system prompt (detected or fallback)
|
|
553
|
-
if "system" not in data:
|
|
554
|
-
# No existing system prompt, inject the detected/fallback one
|
|
555
|
-
data["system"] = detected_system
|
|
556
|
-
else:
|
|
557
|
-
# Request has existing system prompt, prepend the detected/fallback one
|
|
558
|
-
existing_system = data["system"]
|
|
559
|
-
|
|
560
|
-
if isinstance(detected_system, str):
|
|
561
|
-
# Detected system is a string
|
|
562
|
-
if isinstance(existing_system, str):
|
|
563
|
-
# Both are strings, convert to list format
|
|
564
|
-
data["system"] = [
|
|
565
|
-
{"type": "text", "text": detected_system},
|
|
566
|
-
{"type": "text", "text": existing_system},
|
|
567
|
-
]
|
|
568
|
-
elif isinstance(existing_system, list):
|
|
569
|
-
# Detected is string, existing is list
|
|
570
|
-
data["system"] = [
|
|
571
|
-
{"type": "text", "text": detected_system}
|
|
572
|
-
] + existing_system
|
|
573
|
-
elif isinstance(detected_system, list):
|
|
574
|
-
# Detected system is a list
|
|
575
|
-
if isinstance(existing_system, str):
|
|
576
|
-
# Detected is list, existing is string
|
|
577
|
-
data["system"] = detected_system + [
|
|
578
|
-
{"type": "text", "text": existing_system}
|
|
579
|
-
]
|
|
580
|
-
elif isinstance(existing_system, list):
|
|
581
|
-
# Both are lists, concatenate
|
|
582
|
-
data["system"] = detected_system + existing_system
|
|
583
|
-
|
|
584
|
-
# Limit cache_control blocks to comply with Anthropic's limit
|
|
585
|
-
data = self._limit_cache_control_blocks(data)
|
|
586
|
-
|
|
587
|
-
return json.dumps(data).encode("utf-8")
|
|
588
|
-
|
|
589
|
-
def _is_openai_request(self, path: str, body: bytes) -> bool:
|
|
590
|
-
"""Check if this is an OpenAI API request."""
|
|
591
|
-
# Check path-based indicators
|
|
592
|
-
if "/openai/" in path or "/chat/completions" in path:
|
|
593
|
-
return True
|
|
594
|
-
|
|
595
|
-
# Check body-based indicators
|
|
596
|
-
if body:
|
|
597
|
-
try:
|
|
598
|
-
import json
|
|
599
|
-
|
|
600
|
-
data = json.loads(body.decode("utf-8"))
|
|
601
|
-
# Look for OpenAI-specific patterns
|
|
602
|
-
model = data.get("model", "")
|
|
603
|
-
if model.startswith(("gpt-", "o1-", "text-davinci")):
|
|
604
|
-
return True
|
|
605
|
-
# Check for OpenAI message format with system in messages
|
|
606
|
-
messages = data.get("messages", [])
|
|
607
|
-
if messages and any(msg.get("role") == "system" for msg in messages):
|
|
608
|
-
return True
|
|
609
|
-
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
610
|
-
logger.warning(
|
|
611
|
-
"openai_request_detection_json_decode_failed",
|
|
612
|
-
error=str(e),
|
|
613
|
-
body_preview=body[:100].decode("utf-8", errors="replace")
|
|
614
|
-
if body
|
|
615
|
-
else None,
|
|
616
|
-
)
|
|
617
|
-
pass
|
|
618
|
-
|
|
619
|
-
return False
|
|
620
|
-
|
|
621
|
-
def _transform_openai_to_anthropic(self, body: bytes) -> bytes:
|
|
622
|
-
"""Transform OpenAI request format to Anthropic format."""
|
|
623
|
-
try:
|
|
624
|
-
# Use the OpenAI adapter for transformation
|
|
625
|
-
import json
|
|
626
|
-
|
|
627
|
-
from ccproxy.adapters.openai.adapter import OpenAIAdapter
|
|
628
|
-
|
|
629
|
-
adapter = OpenAIAdapter()
|
|
630
|
-
openai_data = json.loads(body.decode("utf-8"))
|
|
631
|
-
anthropic_data = adapter.adapt_request(openai_data)
|
|
632
|
-
return json.dumps(anthropic_data).encode("utf-8")
|
|
633
|
-
|
|
634
|
-
except Exception as e:
|
|
635
|
-
logger.warning(
|
|
636
|
-
"openai_transformation_failed",
|
|
637
|
-
error=str(e),
|
|
638
|
-
operation="transform_openai_to_anthropic",
|
|
639
|
-
)
|
|
640
|
-
# Return original body if transformation fails
|
|
641
|
-
return body
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
class HTTPResponseTransformer(ResponseTransformer):
|
|
645
|
-
"""HTTP response transformer that implements the abstract ResponseTransformer interface."""
|
|
646
|
-
|
|
647
|
-
def __init__(self) -> None:
|
|
648
|
-
"""Initialize HTTP response transformer."""
|
|
649
|
-
super().__init__()
|
|
650
|
-
|
|
651
|
-
async def _transform_response(
|
|
652
|
-
self, response: ProxyResponse, context: TransformContext | None = None
|
|
653
|
-
) -> ProxyResponse:
|
|
654
|
-
"""Transform a proxy response according to the abstract interface.
|
|
655
|
-
|
|
656
|
-
Args:
|
|
657
|
-
response: The structured proxy response to transform
|
|
658
|
-
context: Optional transformation context
|
|
659
|
-
|
|
660
|
-
Returns:
|
|
661
|
-
The transformed proxy response
|
|
662
|
-
"""
|
|
663
|
-
# Extract original path from context for transformation decisions
|
|
664
|
-
original_path = ""
|
|
665
|
-
if context and hasattr(context, "original_path"):
|
|
666
|
-
original_path = context.original_path
|
|
667
|
-
elif context and isinstance(context, dict):
|
|
668
|
-
original_path = context.get("original_path", "")
|
|
669
|
-
|
|
670
|
-
# Transform response body
|
|
671
|
-
transformed_body = response.body
|
|
672
|
-
if response.body:
|
|
673
|
-
if isinstance(response.body, bytes):
|
|
674
|
-
transformed_body = self.transform_response_body(
|
|
675
|
-
response.body, original_path
|
|
676
|
-
)
|
|
677
|
-
elif isinstance(response.body, str):
|
|
678
|
-
body_bytes = response.body.encode("utf-8")
|
|
679
|
-
transformed_body = self.transform_response_body(
|
|
680
|
-
body_bytes, original_path
|
|
681
|
-
)
|
|
682
|
-
elif isinstance(response.body, dict):
|
|
683
|
-
import json
|
|
684
|
-
|
|
685
|
-
body_bytes = json.dumps(response.body).encode("utf-8")
|
|
686
|
-
transformed_body = self.transform_response_body(
|
|
687
|
-
body_bytes, original_path
|
|
688
|
-
)
|
|
689
|
-
|
|
690
|
-
# Calculate content length for transformed body
|
|
691
|
-
content_length = 0
|
|
692
|
-
if transformed_body:
|
|
693
|
-
if isinstance(transformed_body, bytes):
|
|
694
|
-
content_length = len(transformed_body)
|
|
695
|
-
elif isinstance(transformed_body, str):
|
|
696
|
-
content_length = len(transformed_body.encode("utf-8"))
|
|
697
|
-
else:
|
|
698
|
-
content_length = len(str(transformed_body))
|
|
699
|
-
|
|
700
|
-
# Transform response headers
|
|
701
|
-
transformed_headers = self.transform_response_headers(
|
|
702
|
-
response.headers, original_path, content_length
|
|
703
|
-
)
|
|
704
|
-
|
|
705
|
-
# Create new transformed response
|
|
706
|
-
return ProxyResponse(
|
|
707
|
-
status_code=response.status_code,
|
|
708
|
-
headers=transformed_headers,
|
|
709
|
-
body=transformed_body,
|
|
710
|
-
metadata=response.metadata,
|
|
711
|
-
)
|
|
712
|
-
|
|
713
|
-
async def transform_proxy_response(
|
|
714
|
-
self,
|
|
715
|
-
status_code: int,
|
|
716
|
-
headers: dict[str, str],
|
|
717
|
-
body: bytes,
|
|
718
|
-
original_path: str,
|
|
719
|
-
proxy_mode: str = "full",
|
|
720
|
-
) -> ResponseData:
|
|
721
|
-
"""Transform response using direct parameters from ProxyService.
|
|
722
|
-
|
|
723
|
-
This method provides the same functionality as ProxyService._transform_response()
|
|
724
|
-
but is properly located in the transformer layer.
|
|
725
|
-
|
|
726
|
-
Args:
|
|
727
|
-
status_code: HTTP status code
|
|
728
|
-
headers: Response headers
|
|
729
|
-
body: Response body
|
|
730
|
-
original_path: Original request path for context
|
|
731
|
-
proxy_mode: Proxy transformation mode
|
|
732
|
-
|
|
733
|
-
Returns:
|
|
734
|
-
Dictionary with transformed response data (status_code, headers, body)
|
|
735
|
-
"""
|
|
736
|
-
# For error responses, handle OpenAI transformation if needed
|
|
737
|
-
if status_code >= 400:
|
|
738
|
-
transformed_error_body = body
|
|
739
|
-
if self._is_openai_request(original_path):
|
|
740
|
-
try:
|
|
741
|
-
import json
|
|
742
|
-
|
|
743
|
-
from ccproxy.adapters.openai.adapter import OpenAIAdapter
|
|
744
|
-
|
|
745
|
-
error_data = json.loads(body.decode("utf-8"))
|
|
746
|
-
openai_adapter = OpenAIAdapter()
|
|
747
|
-
openai_error = openai_adapter.adapt_error(error_data)
|
|
748
|
-
transformed_error_body = json.dumps(openai_error).encode("utf-8")
|
|
749
|
-
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
750
|
-
# Keep original error if parsing fails
|
|
751
|
-
pass
|
|
752
|
-
|
|
753
|
-
return ResponseData(
|
|
754
|
-
status_code=status_code,
|
|
755
|
-
headers=headers,
|
|
756
|
-
body=transformed_error_body,
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
# For successful responses, transform normally
|
|
760
|
-
transformed_body = self.transform_response_body(body, original_path, proxy_mode)
|
|
761
|
-
|
|
762
|
-
transformed_headers = self.transform_response_headers(
|
|
763
|
-
headers, original_path, len(transformed_body), proxy_mode
|
|
764
|
-
)
|
|
765
|
-
|
|
766
|
-
return ResponseData(
|
|
767
|
-
status_code=status_code,
|
|
768
|
-
headers=transformed_headers,
|
|
769
|
-
body=transformed_body,
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
def transform_response_body(
|
|
773
|
-
self, body: bytes, path: str, proxy_mode: str = "full"
|
|
774
|
-
) -> bytes:
|
|
775
|
-
"""Transform response body."""
|
|
776
|
-
# Basic body transformation - pass through for now
|
|
777
|
-
return body
|
|
778
|
-
|
|
779
|
-
def transform_response_headers(
|
|
780
|
-
self,
|
|
781
|
-
headers: dict[str, str],
|
|
782
|
-
path: str,
|
|
783
|
-
content_length: int,
|
|
784
|
-
proxy_mode: str = "full",
|
|
785
|
-
) -> dict[str, str]:
|
|
786
|
-
"""Transform response headers."""
|
|
787
|
-
transformed_headers = {}
|
|
788
|
-
|
|
789
|
-
# Copy important headers
|
|
790
|
-
for key, value in headers.items():
|
|
791
|
-
lower_key = key.lower()
|
|
792
|
-
if lower_key not in [
|
|
793
|
-
"content-length",
|
|
794
|
-
"transfer-encoding",
|
|
795
|
-
"content-encoding",
|
|
796
|
-
"date", # Remove upstream date header to avoid conflicts
|
|
797
|
-
]:
|
|
798
|
-
transformed_headers[key] = value
|
|
799
|
-
|
|
800
|
-
# Set content length
|
|
801
|
-
transformed_headers["Content-Length"] = str(content_length)
|
|
802
|
-
|
|
803
|
-
# Add CORS headers
|
|
804
|
-
transformed_headers["Access-Control-Allow-Origin"] = "*"
|
|
805
|
-
transformed_headers["Access-Control-Allow-Headers"] = "*"
|
|
806
|
-
transformed_headers["Access-Control-Allow-Methods"] = "*"
|
|
807
|
-
|
|
808
|
-
return transformed_headers
|
|
809
|
-
|
|
810
|
-
def _is_openai_request(self, path: str) -> bool:
|
|
811
|
-
"""Check if this is an OpenAI API request."""
|
|
812
|
-
return "/openai/" in path or "/chat/completions" in path
|