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
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from starlette.responses import Response, StreamingResponse
|
|
8
|
+
|
|
9
|
+
from ccproxy.auth.exceptions import CredentialsInvalidError, OAuthTokenRefreshError
|
|
10
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
11
|
+
from ccproxy.core.plugins.interfaces import (
|
|
12
|
+
DetectionServiceProtocol,
|
|
13
|
+
TokenManagerProtocol,
|
|
14
|
+
)
|
|
15
|
+
from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
|
|
16
|
+
from ccproxy.utils.headers import (
|
|
17
|
+
extract_response_headers,
|
|
18
|
+
filter_request_headers,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .config import ClaudeAPISettings
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = get_plugin_logger()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClaudeAPIAdapter(BaseHTTPAdapter):
|
|
28
|
+
"""Simplified Claude API adapter."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
detection_service: DetectionServiceProtocol,
|
|
33
|
+
config: ClaudeAPISettings | None = None,
|
|
34
|
+
**kwargs: Any,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(config=config or ClaudeAPISettings(), **kwargs)
|
|
37
|
+
self.detection_service: DetectionServiceProtocol = detection_service
|
|
38
|
+
self.token_manager: TokenManagerProtocol = cast(
|
|
39
|
+
TokenManagerProtocol, self.auth_manager
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self.base_url = self.config.base_url.rstrip("/")
|
|
43
|
+
|
|
44
|
+
async def get_target_url(self, endpoint: str) -> str:
|
|
45
|
+
return f"{self.base_url}/v1/messages"
|
|
46
|
+
|
|
47
|
+
async def prepare_provider_request(
|
|
48
|
+
self, body: bytes, headers: dict[str, str], endpoint: str
|
|
49
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
50
|
+
# Get a valid access token (auto-refreshes if expired)
|
|
51
|
+
token_value = await self._resolve_access_token()
|
|
52
|
+
|
|
53
|
+
# Parse body
|
|
54
|
+
body_data = json.loads(body.decode()) if body else {}
|
|
55
|
+
|
|
56
|
+
# Anthropic API rejects null temperature fields, so strip them early
|
|
57
|
+
if body_data.get("temperature") is None:
|
|
58
|
+
body_data.pop("temperature", None)
|
|
59
|
+
|
|
60
|
+
# Anthropic API constraint: cannot accept both temperature and top_p
|
|
61
|
+
# Prioritize temperature over top_p when both are present
|
|
62
|
+
if "temperature" in body_data and "top_p" in body_data:
|
|
63
|
+
body_data.pop("top_p", None)
|
|
64
|
+
|
|
65
|
+
if self._needs_anthropic_conversion(endpoint):
|
|
66
|
+
body_data = self._convert_openai_to_anthropic(body_data)
|
|
67
|
+
|
|
68
|
+
# Inject system prompt based on config mode using detection service helper
|
|
69
|
+
system_mode = self.config.system_prompt_injection_mode
|
|
70
|
+
if self.detection_service and system_mode != "none":
|
|
71
|
+
system_value = self._resolve_system_prompt_value(system_mode)
|
|
72
|
+
if system_value is not None:
|
|
73
|
+
body_data = self._inject_system_prompt(
|
|
74
|
+
body_data, system_value, mode=system_mode
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Limit cache_control blocks to comply with Anthropic's limit
|
|
78
|
+
body_data = self._limit_cache_control_blocks(body_data)
|
|
79
|
+
|
|
80
|
+
# Remove internal metadata fields like _ccproxy_injected before sending to the API
|
|
81
|
+
body_data = self._remove_metadata_fields(body_data)
|
|
82
|
+
|
|
83
|
+
# Filter headers and enforce OAuth Authorization
|
|
84
|
+
filtered_headers = filter_request_headers(headers, preserve_auth=False)
|
|
85
|
+
# Always set Authorization from OAuth-managed access token
|
|
86
|
+
filtered_headers["authorization"] = f"Bearer {token_value}"
|
|
87
|
+
|
|
88
|
+
# Add CLI headers if available, but never allow overriding auth
|
|
89
|
+
cli_headers = self._collect_cli_headers()
|
|
90
|
+
if cli_headers:
|
|
91
|
+
blocked_overrides = {"authorization", "x-api-key"}
|
|
92
|
+
for key, value in cli_headers.items():
|
|
93
|
+
lk = key.lower()
|
|
94
|
+
if lk in blocked_overrides:
|
|
95
|
+
logger.debug(
|
|
96
|
+
"cli_header_override_blocked",
|
|
97
|
+
header=lk,
|
|
98
|
+
reason="preserve_oauth_auth_header",
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
filtered_headers[lk] = value
|
|
102
|
+
|
|
103
|
+
return json.dumps(body_data).encode(), filtered_headers
|
|
104
|
+
|
|
105
|
+
async def process_provider_response(
|
|
106
|
+
self, response: httpx.Response, endpoint: str
|
|
107
|
+
) -> Response | StreamingResponse:
|
|
108
|
+
"""Return a plain Response; streaming handled upstream by BaseHTTPAdapter.
|
|
109
|
+
|
|
110
|
+
The BaseHTTPAdapter is responsible for detecting streaming and delegating
|
|
111
|
+
to the shared StreamingHandler. For non-streaming responses, adapters
|
|
112
|
+
should return a simple Starlette Response.
|
|
113
|
+
"""
|
|
114
|
+
response_headers = extract_response_headers(response)
|
|
115
|
+
|
|
116
|
+
body_bytes = response.content
|
|
117
|
+
media_type = response.headers.get("content-type")
|
|
118
|
+
|
|
119
|
+
if self._needs_openai_conversion(endpoint):
|
|
120
|
+
converted = self._convert_anthropic_to_openai_response(response)
|
|
121
|
+
if converted is not None:
|
|
122
|
+
body_bytes = json.dumps(converted).encode()
|
|
123
|
+
media_type = "application/json"
|
|
124
|
+
|
|
125
|
+
return Response(
|
|
126
|
+
content=body_bytes,
|
|
127
|
+
status_code=response.status_code,
|
|
128
|
+
headers=response_headers,
|
|
129
|
+
media_type=media_type,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _needs_openai_conversion(self, endpoint: str) -> bool:
|
|
133
|
+
if not getattr(self.config, "support_openai_format", True):
|
|
134
|
+
return False
|
|
135
|
+
normalized = (endpoint or "").strip().lower()
|
|
136
|
+
return normalized.startswith("/v1/chat/completions")
|
|
137
|
+
|
|
138
|
+
def _needs_anthropic_conversion(self, endpoint: str) -> bool:
|
|
139
|
+
if not getattr(self.config, "support_openai_format", True):
|
|
140
|
+
return False
|
|
141
|
+
normalized = (endpoint or "").strip().lower()
|
|
142
|
+
return normalized.startswith("/v1/chat/completions")
|
|
143
|
+
|
|
144
|
+
async def _resolve_access_token(self) -> str:
|
|
145
|
+
"""Resolve a usable Claude API OAuth token from the token manager.
|
|
146
|
+
|
|
147
|
+
If the auth manager is not configured, raise a unified AuthenticationError
|
|
148
|
+
so middleware returns a clean 401 without stack traces.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
if not getattr(self, "token_manager", None):
|
|
152
|
+
from ccproxy.core.errors import AuthenticationError
|
|
153
|
+
|
|
154
|
+
logger.warning(
|
|
155
|
+
"auth_manager_override_not_resolved",
|
|
156
|
+
plugin="claude_api",
|
|
157
|
+
auth_manager_name="oauth_claude",
|
|
158
|
+
category="auth",
|
|
159
|
+
)
|
|
160
|
+
raise AuthenticationError(
|
|
161
|
+
"Authentication manager not configured for Claude API provider"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
token_manager = self.token_manager
|
|
165
|
+
|
|
166
|
+
async def _snapshot_token() -> str | None:
|
|
167
|
+
snapshot = await token_manager.get_token_snapshot()
|
|
168
|
+
if snapshot and snapshot.access_token:
|
|
169
|
+
return str(snapshot.access_token)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
credentials = await token_manager.load_credentials()
|
|
173
|
+
if credentials and token_manager.should_refresh(credentials):
|
|
174
|
+
try:
|
|
175
|
+
refreshed = await token_manager.get_access_token_with_refresh()
|
|
176
|
+
if refreshed:
|
|
177
|
+
return refreshed
|
|
178
|
+
except OAuthTokenRefreshError as exc:
|
|
179
|
+
logger.warning(
|
|
180
|
+
"claude_token_refresh_failed",
|
|
181
|
+
error=str(exc),
|
|
182
|
+
category="auth",
|
|
183
|
+
)
|
|
184
|
+
fallback = await _snapshot_token()
|
|
185
|
+
if fallback:
|
|
186
|
+
return fallback
|
|
187
|
+
|
|
188
|
+
# Primary path: rely on manager contract
|
|
189
|
+
try:
|
|
190
|
+
token = await token_manager.get_access_token()
|
|
191
|
+
if token:
|
|
192
|
+
return token
|
|
193
|
+
except CredentialsInvalidError:
|
|
194
|
+
logger.debug("claude_token_invalid", category="auth")
|
|
195
|
+
except OAuthTokenRefreshError as exc:
|
|
196
|
+
logger.warning(
|
|
197
|
+
"claude_token_refresh_failed",
|
|
198
|
+
error=str(exc),
|
|
199
|
+
category="auth",
|
|
200
|
+
)
|
|
201
|
+
fallback = await _snapshot_token()
|
|
202
|
+
if fallback:
|
|
203
|
+
return fallback
|
|
204
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
205
|
+
logger.debug(
|
|
206
|
+
"claude_token_fetch_failed",
|
|
207
|
+
error=str(exc),
|
|
208
|
+
category="auth",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Fallback to explicit refresh helper
|
|
212
|
+
try:
|
|
213
|
+
refreshed = await token_manager.get_access_token_with_refresh()
|
|
214
|
+
if refreshed:
|
|
215
|
+
return refreshed
|
|
216
|
+
except OAuthTokenRefreshError as exc:
|
|
217
|
+
logger.warning(
|
|
218
|
+
"claude_token_refresh_failed",
|
|
219
|
+
error=str(exc),
|
|
220
|
+
category="auth",
|
|
221
|
+
)
|
|
222
|
+
fallback = await _snapshot_token()
|
|
223
|
+
if fallback:
|
|
224
|
+
return fallback
|
|
225
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
226
|
+
logger.debug(
|
|
227
|
+
"claude_token_refresh_failed",
|
|
228
|
+
error=str(exc),
|
|
229
|
+
category="auth",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
fallback = await _snapshot_token()
|
|
233
|
+
if fallback:
|
|
234
|
+
return fallback
|
|
235
|
+
|
|
236
|
+
raise ValueError("No valid OAuth access token available for Claude API")
|
|
237
|
+
|
|
238
|
+
def _resolve_system_prompt_value(self, mode: str) -> Any:
|
|
239
|
+
"""Retrieve system prompt content for injection from detection cache."""
|
|
240
|
+
|
|
241
|
+
if not self.detection_service:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
# Primary path: detection service helper
|
|
245
|
+
try:
|
|
246
|
+
prompt = self.detection_service.get_system_prompt(mode=mode)
|
|
247
|
+
except Exception:
|
|
248
|
+
prompt = {}
|
|
249
|
+
if isinstance(prompt, dict):
|
|
250
|
+
system_value = prompt.get("system")
|
|
251
|
+
if system_value:
|
|
252
|
+
return system_value
|
|
253
|
+
|
|
254
|
+
prompts = self.detection_service.get_detected_prompts()
|
|
255
|
+
if prompts.has_system():
|
|
256
|
+
system_payload = prompts.system_payload(mode=mode)
|
|
257
|
+
system_value = system_payload.get("system") if system_payload else None
|
|
258
|
+
if system_value:
|
|
259
|
+
return system_value
|
|
260
|
+
return prompts.system
|
|
261
|
+
|
|
262
|
+
cached = self.detection_service.get_cached_data()
|
|
263
|
+
# Backward compatibility: legacy cached.system_prompt object
|
|
264
|
+
system_prompt_obj = getattr(cached, "system_prompt", None) if cached else None
|
|
265
|
+
if system_prompt_obj is not None:
|
|
266
|
+
return getattr(system_prompt_obj, "system_field", system_prompt_obj)
|
|
267
|
+
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def _collect_cli_headers(self) -> dict[str, str]:
|
|
271
|
+
"""Collect safe CLI headers from detection cache for request forwarding."""
|
|
272
|
+
|
|
273
|
+
if not self.detection_service:
|
|
274
|
+
return {}
|
|
275
|
+
|
|
276
|
+
headers_data = self.detection_service.get_detected_headers()
|
|
277
|
+
if not headers_data:
|
|
278
|
+
return {}
|
|
279
|
+
|
|
280
|
+
ignores = {h.lower() for h in self.detection_service.get_ignored_headers()}
|
|
281
|
+
redacted = {h.lower() for h in self.detection_service.get_redacted_headers()}
|
|
282
|
+
|
|
283
|
+
return headers_data.filtered(ignores=ignores, redacted=redacted)
|
|
284
|
+
|
|
285
|
+
def _convert_openai_to_anthropic(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
286
|
+
"""Convert an OpenAI chat.completions style payload to Anthropic format."""
|
|
287
|
+
|
|
288
|
+
if not isinstance(payload, dict):
|
|
289
|
+
return payload
|
|
290
|
+
|
|
291
|
+
messages = payload.get("messages")
|
|
292
|
+
if not isinstance(messages, list):
|
|
293
|
+
return payload
|
|
294
|
+
|
|
295
|
+
system_blocks: list[Any] = []
|
|
296
|
+
anthropic_messages: list[dict[str, Any]] = []
|
|
297
|
+
|
|
298
|
+
for msg in messages:
|
|
299
|
+
if not isinstance(msg, dict):
|
|
300
|
+
continue
|
|
301
|
+
role = msg.get("role")
|
|
302
|
+
content = msg.get("content", "")
|
|
303
|
+
|
|
304
|
+
if role == "system":
|
|
305
|
+
block = self._normalize_text_block(content)
|
|
306
|
+
if block is not None:
|
|
307
|
+
if isinstance(block, list):
|
|
308
|
+
system_blocks.extend(block)
|
|
309
|
+
else:
|
|
310
|
+
system_blocks.append(block)
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
block = self._normalize_text_block(content)
|
|
314
|
+
if block is None:
|
|
315
|
+
block = []
|
|
316
|
+
|
|
317
|
+
anthropic_messages.append(
|
|
318
|
+
{
|
|
319
|
+
"role": role or "user",
|
|
320
|
+
"content": block if isinstance(block, list) else [block],
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
converted = {k: v for k, v in payload.items() if k != "messages"}
|
|
325
|
+
if system_blocks:
|
|
326
|
+
converted["system"] = system_blocks
|
|
327
|
+
converted["messages"] = anthropic_messages
|
|
328
|
+
return converted
|
|
329
|
+
|
|
330
|
+
def _normalize_text_block(self, content: Any) -> Any:
|
|
331
|
+
if isinstance(content, list):
|
|
332
|
+
blocks = []
|
|
333
|
+
for part in content:
|
|
334
|
+
if isinstance(part, dict):
|
|
335
|
+
blocks.append(part)
|
|
336
|
+
elif isinstance(part, str):
|
|
337
|
+
blocks.append({"type": "text", "text": part})
|
|
338
|
+
return blocks
|
|
339
|
+
if isinstance(content, dict):
|
|
340
|
+
return content
|
|
341
|
+
if isinstance(content, str):
|
|
342
|
+
return {"type": "text", "text": content}
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
def _convert_anthropic_to_openai_response(
|
|
346
|
+
self, response: httpx.Response
|
|
347
|
+
) -> dict[str, Any] | None:
|
|
348
|
+
try:
|
|
349
|
+
payload = json.loads(response.content.decode()) if response.content else {}
|
|
350
|
+
except json.JSONDecodeError:
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
if not isinstance(payload, dict):
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
message = {
|
|
357
|
+
"role": "assistant",
|
|
358
|
+
"content": "",
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
content_blocks = payload.get("content")
|
|
362
|
+
if isinstance(content_blocks, list):
|
|
363
|
+
texts = [
|
|
364
|
+
block.get("text", "")
|
|
365
|
+
for block in content_blocks
|
|
366
|
+
if isinstance(block, dict) and block.get("type") == "text"
|
|
367
|
+
]
|
|
368
|
+
message["content"] = "".join(texts)
|
|
369
|
+
elif isinstance(content_blocks, str):
|
|
370
|
+
message["content"] = content_blocks
|
|
371
|
+
|
|
372
|
+
finish_reason = payload.get("stop_reason") or payload.get("stop_sequence")
|
|
373
|
+
finish_reason_map = {
|
|
374
|
+
"end_turn": "stop",
|
|
375
|
+
"stop_sequence": "stop",
|
|
376
|
+
"max_tokens": "length",
|
|
377
|
+
}
|
|
378
|
+
if isinstance(finish_reason, str):
|
|
379
|
+
mapped_finish_reason = finish_reason_map.get(finish_reason, "stop")
|
|
380
|
+
else:
|
|
381
|
+
mapped_finish_reason = "stop"
|
|
382
|
+
|
|
383
|
+
usage = payload.get("usage")
|
|
384
|
+
usage_converted = None
|
|
385
|
+
if isinstance(usage, dict):
|
|
386
|
+
prompt_tokens = int(usage.get("input_tokens") or 0)
|
|
387
|
+
completion_tokens = int(usage.get("output_tokens") or 0)
|
|
388
|
+
total_tokens = int(
|
|
389
|
+
usage.get("total_tokens") or prompt_tokens + completion_tokens
|
|
390
|
+
)
|
|
391
|
+
usage_converted = {
|
|
392
|
+
"prompt_tokens": prompt_tokens,
|
|
393
|
+
"completion_tokens": completion_tokens,
|
|
394
|
+
"total_tokens": total_tokens,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
model_value = payload.get("model")
|
|
398
|
+
if not isinstance(model_value, str) or not model_value:
|
|
399
|
+
mappings = getattr(self.config, "model_mappings", None) or []
|
|
400
|
+
if mappings:
|
|
401
|
+
model_value = mappings[0].target
|
|
402
|
+
else:
|
|
403
|
+
model_value = ""
|
|
404
|
+
|
|
405
|
+
converted = {
|
|
406
|
+
"id": payload.get("id") or f"chatcmpl-{uuid.uuid4().hex}",
|
|
407
|
+
"object": "chat.completion",
|
|
408
|
+
"created": int(time.time()),
|
|
409
|
+
"model": model_value,
|
|
410
|
+
"choices": [
|
|
411
|
+
{
|
|
412
|
+
"index": 0,
|
|
413
|
+
"message": message,
|
|
414
|
+
"finish_reason": mapped_finish_reason,
|
|
415
|
+
"logprobs": None,
|
|
416
|
+
}
|
|
417
|
+
],
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if usage_converted is not None:
|
|
421
|
+
converted["usage"] = usage_converted
|
|
422
|
+
|
|
423
|
+
return converted
|
|
424
|
+
|
|
425
|
+
# Helper methods (move from transformers)
|
|
426
|
+
def _inject_system_prompt(
|
|
427
|
+
self, body_data: dict[str, Any], system_prompt: Any, mode: str = "full"
|
|
428
|
+
) -> dict[str, Any]:
|
|
429
|
+
"""Inject system prompt from Claude CLI detection.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
body_data: The request body data dict
|
|
433
|
+
system_prompt: System prompt data from detection service
|
|
434
|
+
mode: Injection mode - "full" (all prompts), "minimal" (first prompt only), or "none"
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Modified body data with system prompt injected
|
|
438
|
+
"""
|
|
439
|
+
if not system_prompt:
|
|
440
|
+
return body_data
|
|
441
|
+
|
|
442
|
+
# Get the system field from the system prompt data
|
|
443
|
+
system_field = (
|
|
444
|
+
system_prompt.system_field
|
|
445
|
+
if hasattr(system_prompt, "system_field")
|
|
446
|
+
else system_prompt
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if not system_field:
|
|
450
|
+
return body_data
|
|
451
|
+
|
|
452
|
+
# Apply injection mode filtering
|
|
453
|
+
if mode == "minimal":
|
|
454
|
+
# Only inject the first system prompt block
|
|
455
|
+
if isinstance(system_field, list) and len(system_field) > 0:
|
|
456
|
+
system_field = [system_field[0]]
|
|
457
|
+
# If it's a string, keep as-is (already minimal)
|
|
458
|
+
elif mode == "none":
|
|
459
|
+
# Should not reach here due to earlier check, but handle gracefully
|
|
460
|
+
return body_data
|
|
461
|
+
# For "full" mode, use system_field as-is
|
|
462
|
+
|
|
463
|
+
# Mark the detected system prompt as injected for preservation
|
|
464
|
+
marked_system = self._mark_injected_system_prompts(system_field)
|
|
465
|
+
|
|
466
|
+
existing_system = body_data.get("system")
|
|
467
|
+
|
|
468
|
+
if existing_system is None:
|
|
469
|
+
# No existing system prompt, inject the marked detected one
|
|
470
|
+
body_data["system"] = marked_system
|
|
471
|
+
else:
|
|
472
|
+
# Request has existing system prompt, prepend the marked detected one
|
|
473
|
+
if isinstance(marked_system, list):
|
|
474
|
+
if isinstance(existing_system, str):
|
|
475
|
+
# Detected is marked list, existing is string
|
|
476
|
+
body_data["system"] = marked_system + [
|
|
477
|
+
{"type": "text", "text": existing_system}
|
|
478
|
+
]
|
|
479
|
+
elif isinstance(existing_system, list):
|
|
480
|
+
# Both are lists, concatenate (detected first)
|
|
481
|
+
body_data["system"] = marked_system + existing_system
|
|
482
|
+
else:
|
|
483
|
+
# Convert both to list format for consistency
|
|
484
|
+
if isinstance(existing_system, str):
|
|
485
|
+
body_data["system"] = [
|
|
486
|
+
{
|
|
487
|
+
"type": "text",
|
|
488
|
+
"text": str(marked_system),
|
|
489
|
+
"_ccproxy_injected": True,
|
|
490
|
+
},
|
|
491
|
+
{"type": "text", "text": existing_system},
|
|
492
|
+
]
|
|
493
|
+
elif isinstance(existing_system, list):
|
|
494
|
+
body_data["system"] = [
|
|
495
|
+
{
|
|
496
|
+
"type": "text",
|
|
497
|
+
"text": str(marked_system),
|
|
498
|
+
"_ccproxy_injected": True,
|
|
499
|
+
}
|
|
500
|
+
] + existing_system
|
|
501
|
+
|
|
502
|
+
return body_data
|
|
503
|
+
|
|
504
|
+
def _mark_injected_system_prompts(self, system_data: Any) -> Any:
|
|
505
|
+
"""Mark system prompts as injected by ccproxy for preservation.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
system_data: System prompt data to mark
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
System data with injected blocks marked with _ccproxy_injected metadata
|
|
512
|
+
"""
|
|
513
|
+
if isinstance(system_data, str):
|
|
514
|
+
# String format - convert to list with marking
|
|
515
|
+
return [{"type": "text", "text": system_data, "_ccproxy_injected": True}]
|
|
516
|
+
elif isinstance(system_data, list):
|
|
517
|
+
# List format - mark each block as injected
|
|
518
|
+
marked_data = []
|
|
519
|
+
for block in system_data:
|
|
520
|
+
if isinstance(block, dict):
|
|
521
|
+
# Copy block and add marking
|
|
522
|
+
marked_block = block.copy()
|
|
523
|
+
marked_block["_ccproxy_injected"] = True
|
|
524
|
+
marked_data.append(marked_block)
|
|
525
|
+
else:
|
|
526
|
+
# Preserve non-dict blocks as-is
|
|
527
|
+
marked_data.append(block)
|
|
528
|
+
return marked_data
|
|
529
|
+
|
|
530
|
+
return system_data
|
|
531
|
+
|
|
532
|
+
def _remove_metadata_fields(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
533
|
+
"""Remove internal ccproxy metadata from request data before sending to API.
|
|
534
|
+
|
|
535
|
+
This method removes:
|
|
536
|
+
- Fields starting with '_' (internal metadata like _ccproxy_injected)
|
|
537
|
+
- Any other internal ccproxy metadata that shouldn't be sent to the API
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
data: Request data dictionary
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Cleaned data dictionary without internal metadata
|
|
544
|
+
"""
|
|
545
|
+
import copy
|
|
546
|
+
|
|
547
|
+
# Deep copy to avoid modifying original
|
|
548
|
+
clean_data = copy.deepcopy(data)
|
|
549
|
+
|
|
550
|
+
# Clean system field
|
|
551
|
+
system = clean_data.get("system")
|
|
552
|
+
if isinstance(system, list):
|
|
553
|
+
for block in system:
|
|
554
|
+
if isinstance(block, dict) and "_ccproxy_injected" in block:
|
|
555
|
+
del block["_ccproxy_injected"]
|
|
556
|
+
|
|
557
|
+
# Clean messages
|
|
558
|
+
messages = clean_data.get("messages", [])
|
|
559
|
+
for message in messages:
|
|
560
|
+
content = message.get("content")
|
|
561
|
+
if isinstance(content, list):
|
|
562
|
+
for block in content:
|
|
563
|
+
if isinstance(block, dict) and "_ccproxy_injected" in block:
|
|
564
|
+
del block["_ccproxy_injected"]
|
|
565
|
+
|
|
566
|
+
# Clean tools (though they shouldn't have _ccproxy_injected, but be safe)
|
|
567
|
+
tools = clean_data.get("tools", [])
|
|
568
|
+
for tool in tools:
|
|
569
|
+
if isinstance(tool, dict) and "_ccproxy_injected" in tool:
|
|
570
|
+
del tool["_ccproxy_injected"]
|
|
571
|
+
|
|
572
|
+
return clean_data
|
|
573
|
+
|
|
574
|
+
def _find_cache_control_blocks(
|
|
575
|
+
self, data: dict[str, Any]
|
|
576
|
+
) -> list[tuple[str, int, int]]:
|
|
577
|
+
"""Find all cache_control blocks in the request with their locations.
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
List of tuples (location_type, location_index, block_index) for each cache_control block
|
|
581
|
+
where location_type is 'system', 'message', 'tool', 'tool_use', or 'tool_result'
|
|
582
|
+
"""
|
|
583
|
+
blocks = []
|
|
584
|
+
|
|
585
|
+
# Find in system field
|
|
586
|
+
system = data.get("system")
|
|
587
|
+
if isinstance(system, list):
|
|
588
|
+
for i, block in enumerate(system):
|
|
589
|
+
if isinstance(block, dict) and "cache_control" in block:
|
|
590
|
+
blocks.append(("system", 0, i))
|
|
591
|
+
|
|
592
|
+
# Find in messages
|
|
593
|
+
messages = data.get("messages", [])
|
|
594
|
+
for msg_idx, msg in enumerate(messages):
|
|
595
|
+
content = msg.get("content")
|
|
596
|
+
if isinstance(content, list):
|
|
597
|
+
for block_idx, block in enumerate(content):
|
|
598
|
+
if isinstance(block, dict) and "cache_control" in block:
|
|
599
|
+
block_type = block.get("type")
|
|
600
|
+
if block_type == "tool_use":
|
|
601
|
+
blocks.append(("tool_use", msg_idx, block_idx))
|
|
602
|
+
elif block_type == "tool_result":
|
|
603
|
+
blocks.append(("tool_result", msg_idx, block_idx))
|
|
604
|
+
else:
|
|
605
|
+
blocks.append(("message", msg_idx, block_idx))
|
|
606
|
+
|
|
607
|
+
# Find in tools
|
|
608
|
+
tools = data.get("tools", [])
|
|
609
|
+
for tool_idx, tool in enumerate(tools):
|
|
610
|
+
if isinstance(tool, dict) and "cache_control" in tool:
|
|
611
|
+
blocks.append(("tool", tool_idx, 0))
|
|
612
|
+
|
|
613
|
+
return blocks
|
|
614
|
+
|
|
615
|
+
def _calculate_content_size(self, data: dict[str, Any]) -> int:
|
|
616
|
+
"""Calculate the approximate content size of a block for cache prioritization.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
data: Block data dictionary
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
Approximate size in characters
|
|
623
|
+
"""
|
|
624
|
+
size = 0
|
|
625
|
+
|
|
626
|
+
# Count text content
|
|
627
|
+
if "text" in data:
|
|
628
|
+
size += len(str(data["text"]))
|
|
629
|
+
|
|
630
|
+
# Count tool use content
|
|
631
|
+
if "name" in data: # Tool use block
|
|
632
|
+
size += len(str(data["name"]))
|
|
633
|
+
if "input" in data:
|
|
634
|
+
size += len(str(data["input"]))
|
|
635
|
+
|
|
636
|
+
# Count tool result content
|
|
637
|
+
if "content" in data and isinstance(data["content"], str | list):
|
|
638
|
+
if isinstance(data["content"], str):
|
|
639
|
+
size += len(data["content"])
|
|
640
|
+
else:
|
|
641
|
+
# Nested content - recursively calculate
|
|
642
|
+
for sub_item in data["content"]:
|
|
643
|
+
if isinstance(sub_item, dict):
|
|
644
|
+
size += self._calculate_content_size(sub_item)
|
|
645
|
+
else:
|
|
646
|
+
size += len(str(sub_item))
|
|
647
|
+
|
|
648
|
+
# Count other string fields
|
|
649
|
+
for key, value in data.items():
|
|
650
|
+
if key not in (
|
|
651
|
+
"text",
|
|
652
|
+
"name",
|
|
653
|
+
"input",
|
|
654
|
+
"content",
|
|
655
|
+
"cache_control",
|
|
656
|
+
"_ccproxy_injected",
|
|
657
|
+
"type",
|
|
658
|
+
):
|
|
659
|
+
size += len(str(value))
|
|
660
|
+
|
|
661
|
+
return size
|
|
662
|
+
|
|
663
|
+
def _get_block_at_location(
|
|
664
|
+
self,
|
|
665
|
+
data: dict[str, Any],
|
|
666
|
+
location_type: str,
|
|
667
|
+
location_index: int,
|
|
668
|
+
block_index: int,
|
|
669
|
+
) -> dict[str, Any] | None:
|
|
670
|
+
"""Get the block at a specific location in the data structure.
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
Block dictionary or None if not found
|
|
674
|
+
"""
|
|
675
|
+
if location_type == "system":
|
|
676
|
+
system = data.get("system")
|
|
677
|
+
if isinstance(system, list) and block_index < len(system):
|
|
678
|
+
block = system[block_index]
|
|
679
|
+
return block if isinstance(block, dict) else None
|
|
680
|
+
elif location_type in ("message", "tool_use", "tool_result"):
|
|
681
|
+
messages = data.get("messages", [])
|
|
682
|
+
if location_index < len(messages):
|
|
683
|
+
content = messages[location_index].get("content")
|
|
684
|
+
if isinstance(content, list) and block_index < len(content):
|
|
685
|
+
block = content[block_index]
|
|
686
|
+
return block if isinstance(block, dict) else None
|
|
687
|
+
elif location_type == "tool":
|
|
688
|
+
tools = data.get("tools", [])
|
|
689
|
+
if location_index < len(tools):
|
|
690
|
+
tool = tools[location_index]
|
|
691
|
+
return tool if isinstance(tool, dict) else None
|
|
692
|
+
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
def _remove_cache_control_at_location(
|
|
696
|
+
self,
|
|
697
|
+
data: dict[str, Any],
|
|
698
|
+
location_type: str,
|
|
699
|
+
location_index: int,
|
|
700
|
+
block_index: int,
|
|
701
|
+
) -> bool:
|
|
702
|
+
"""Remove cache_control from a block at a specific location.
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
True if cache_control was successfully removed, False otherwise
|
|
706
|
+
"""
|
|
707
|
+
block = self._get_block_at_location(
|
|
708
|
+
data, location_type, location_index, block_index
|
|
709
|
+
)
|
|
710
|
+
if block and isinstance(block, dict) and "cache_control" in block:
|
|
711
|
+
del block["cache_control"]
|
|
712
|
+
return True
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
def _limit_cache_control_blocks(
|
|
716
|
+
self, data: dict[str, Any], max_blocks: int = 4
|
|
717
|
+
) -> dict[str, Any]:
|
|
718
|
+
"""Limit the number of cache_control blocks using smart algorithm.
|
|
719
|
+
|
|
720
|
+
Smart algorithm:
|
|
721
|
+
1. Preserve all injected system prompts (marked with _ccproxy_injected)
|
|
722
|
+
2. Keep the 2 largest remaining blocks by content size
|
|
723
|
+
3. Remove cache_control from smaller blocks when exceeding the limit
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
data: Request data dictionary
|
|
727
|
+
max_blocks: Maximum number of cache_control blocks allowed (default: 4)
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
Modified data dictionary with cache_control blocks limited
|
|
731
|
+
"""
|
|
732
|
+
import copy
|
|
733
|
+
|
|
734
|
+
# Deep copy to avoid modifying original
|
|
735
|
+
data = copy.deepcopy(data)
|
|
736
|
+
|
|
737
|
+
# Find all cache_control blocks
|
|
738
|
+
cache_blocks = self._find_cache_control_blocks(data)
|
|
739
|
+
total_blocks = len(cache_blocks)
|
|
740
|
+
|
|
741
|
+
if total_blocks <= max_blocks:
|
|
742
|
+
# No need to remove anything
|
|
743
|
+
return data
|
|
744
|
+
|
|
745
|
+
logger.warning(
|
|
746
|
+
"cache_control_limit_exceeded",
|
|
747
|
+
total_blocks=total_blocks,
|
|
748
|
+
max_blocks=max_blocks,
|
|
749
|
+
category="transform",
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Classify blocks as injected vs non-injected and calculate sizes
|
|
753
|
+
injected_blocks = []
|
|
754
|
+
non_injected_blocks = []
|
|
755
|
+
|
|
756
|
+
for location in cache_blocks:
|
|
757
|
+
location_type, location_index, block_index = location
|
|
758
|
+
block = self._get_block_at_location(
|
|
759
|
+
data, location_type, location_index, block_index
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
if block and isinstance(block, dict):
|
|
763
|
+
if block.get("_ccproxy_injected", False):
|
|
764
|
+
injected_blocks.append(location)
|
|
765
|
+
logger.debug(
|
|
766
|
+
"found_injected_block",
|
|
767
|
+
location_type=location_type,
|
|
768
|
+
location_index=location_index,
|
|
769
|
+
block_index=block_index,
|
|
770
|
+
category="transform",
|
|
771
|
+
)
|
|
772
|
+
else:
|
|
773
|
+
# Calculate content size for prioritization
|
|
774
|
+
content_size = self._calculate_content_size(block)
|
|
775
|
+
non_injected_blocks.append((location, content_size))
|
|
776
|
+
|
|
777
|
+
# Sort non-injected blocks by size (largest first)
|
|
778
|
+
non_injected_blocks.sort(key=lambda x: x[1], reverse=True)
|
|
779
|
+
|
|
780
|
+
# Determine how many non-injected blocks we can keep
|
|
781
|
+
injected_count = len(injected_blocks)
|
|
782
|
+
remaining_slots = max_blocks - injected_count
|
|
783
|
+
|
|
784
|
+
logger.info(
|
|
785
|
+
"cache_control_smart_limiting",
|
|
786
|
+
total_blocks=total_blocks,
|
|
787
|
+
injected_blocks=injected_count,
|
|
788
|
+
non_injected_blocks=len(non_injected_blocks),
|
|
789
|
+
remaining_slots=remaining_slots,
|
|
790
|
+
max_blocks=max_blocks,
|
|
791
|
+
category="transform",
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
# Keep the largest non-injected blocks up to remaining slots
|
|
795
|
+
blocks_to_keep = set(injected_blocks) # Always keep injected blocks
|
|
796
|
+
if remaining_slots > 0:
|
|
797
|
+
largest_blocks = non_injected_blocks[:remaining_slots]
|
|
798
|
+
blocks_to_keep.update(location for location, size in largest_blocks)
|
|
799
|
+
|
|
800
|
+
logger.debug(
|
|
801
|
+
"keeping_largest_blocks",
|
|
802
|
+
kept_blocks=[(loc, size) for loc, size in largest_blocks],
|
|
803
|
+
category="transform",
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# Remove cache_control from blocks not in the keep set
|
|
807
|
+
blocks_to_remove = [loc for loc in cache_blocks if loc not in blocks_to_keep]
|
|
808
|
+
|
|
809
|
+
for location_type, location_index, block_index in blocks_to_remove:
|
|
810
|
+
if self._remove_cache_control_at_location(
|
|
811
|
+
data, location_type, location_index, block_index
|
|
812
|
+
):
|
|
813
|
+
logger.debug(
|
|
814
|
+
"removed_cache_control_smart",
|
|
815
|
+
location=location_type,
|
|
816
|
+
location_index=location_index,
|
|
817
|
+
block_index=block_index,
|
|
818
|
+
category="transform",
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
logger.info(
|
|
822
|
+
"cache_control_limiting_complete",
|
|
823
|
+
blocks_removed=len(blocks_to_remove),
|
|
824
|
+
blocks_kept=len(blocks_to_keep),
|
|
825
|
+
injected_preserved=injected_count,
|
|
826
|
+
category="transform",
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
return data
|