ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
"""Service for automatically detecting Claude CLI headers at startup."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import socket
|
|
9
|
-
import subprocess
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
import structlog
|
|
13
|
-
from fastapi import FastAPI, Request, Response
|
|
14
|
-
|
|
15
|
-
from ccproxy.config.discovery import get_ccproxy_cache_dir
|
|
16
|
-
from ccproxy.config.settings import Settings
|
|
17
|
-
from ccproxy.models.detection import (
|
|
18
|
-
ClaudeCacheData,
|
|
19
|
-
ClaudeCodeHeaders,
|
|
20
|
-
SystemPromptData,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
logger = structlog.get_logger(__name__)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class ClaudeDetectionService:
|
|
28
|
-
"""Service for automatically detecting Claude CLI headers at startup."""
|
|
29
|
-
|
|
30
|
-
def __init__(self, settings: Settings) -> None:
|
|
31
|
-
"""Initialize Claude detection service."""
|
|
32
|
-
self.settings = settings
|
|
33
|
-
self.cache_dir = get_ccproxy_cache_dir()
|
|
34
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
-
self._cached_data: ClaudeCacheData | None = None
|
|
36
|
-
|
|
37
|
-
async def initialize_detection(self) -> ClaudeCacheData:
|
|
38
|
-
"""Initialize Claude detection at startup."""
|
|
39
|
-
try:
|
|
40
|
-
# Get current Claude version
|
|
41
|
-
current_version = await self._get_claude_version()
|
|
42
|
-
|
|
43
|
-
# Try to load from cache first
|
|
44
|
-
detected_data = self._load_from_cache(current_version)
|
|
45
|
-
cached = detected_data is not None
|
|
46
|
-
if cached:
|
|
47
|
-
logger.debug("detection_claude_headers_debug", version=current_version)
|
|
48
|
-
else:
|
|
49
|
-
# No cache or version changed - detect fresh
|
|
50
|
-
detected_data = await self._detect_claude_headers(current_version)
|
|
51
|
-
# Cache the results
|
|
52
|
-
self._save_to_cache(detected_data)
|
|
53
|
-
|
|
54
|
-
self._cached_data = detected_data
|
|
55
|
-
|
|
56
|
-
logger.info(
|
|
57
|
-
"detection_claude_headers_completed",
|
|
58
|
-
version=current_version,
|
|
59
|
-
cached=cached,
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# TODO: add proper testing without claude cli installed
|
|
63
|
-
if detected_data is None:
|
|
64
|
-
raise ValueError("Claude detection failed")
|
|
65
|
-
return detected_data
|
|
66
|
-
|
|
67
|
-
except Exception as e:
|
|
68
|
-
logger.warning("detection_claude_headers_failed", fallback=True, error=e)
|
|
69
|
-
# Return fallback data
|
|
70
|
-
fallback_data = self._get_fallback_data()
|
|
71
|
-
self._cached_data = fallback_data
|
|
72
|
-
return fallback_data
|
|
73
|
-
|
|
74
|
-
def get_cached_data(self) -> ClaudeCacheData | None:
|
|
75
|
-
"""Get currently cached detection data."""
|
|
76
|
-
return self._cached_data
|
|
77
|
-
|
|
78
|
-
async def _get_claude_version(self) -> str:
|
|
79
|
-
"""Get Claude CLI version."""
|
|
80
|
-
try:
|
|
81
|
-
result = subprocess.run(
|
|
82
|
-
["claude", "--version"],
|
|
83
|
-
capture_output=True,
|
|
84
|
-
text=True,
|
|
85
|
-
timeout=10,
|
|
86
|
-
)
|
|
87
|
-
if result.returncode == 0:
|
|
88
|
-
# Extract version from output like "1.0.60 (Claude Code)"
|
|
89
|
-
version_line = result.stdout.strip()
|
|
90
|
-
if "/" in version_line:
|
|
91
|
-
# Handle "claude-cli/1.0.60" format
|
|
92
|
-
version_line = version_line.split("/")[-1]
|
|
93
|
-
if "(" in version_line:
|
|
94
|
-
# Handle "1.0.60 (Claude Code)" format - extract just the version number
|
|
95
|
-
return version_line.split("(")[0].strip()
|
|
96
|
-
return version_line
|
|
97
|
-
else:
|
|
98
|
-
raise RuntimeError(f"Claude version command failed: {result.stderr}")
|
|
99
|
-
|
|
100
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
|
|
101
|
-
logger.warning("claude_version_detection_failed", error=str(e))
|
|
102
|
-
return "unknown"
|
|
103
|
-
|
|
104
|
-
async def _detect_claude_headers(self, version: str) -> ClaudeCacheData:
|
|
105
|
-
"""Execute Claude CLI with proxy to capture headers and system prompt."""
|
|
106
|
-
# Data captured from the request
|
|
107
|
-
captured_data: dict[str, Any] = {}
|
|
108
|
-
|
|
109
|
-
async def capture_handler(request: Request) -> Response:
|
|
110
|
-
"""Capture the Claude CLI request."""
|
|
111
|
-
captured_data["headers"] = dict(request.headers)
|
|
112
|
-
captured_data["body"] = await request.body()
|
|
113
|
-
# Return a mock response to satisfy Claude CLI
|
|
114
|
-
return Response(
|
|
115
|
-
content='{"type": "message", "content": [{"type": "text", "text": "Test response"}]}',
|
|
116
|
-
media_type="application/json",
|
|
117
|
-
status_code=200,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# Create temporary FastAPI app
|
|
121
|
-
temp_app = FastAPI()
|
|
122
|
-
temp_app.post("/v1/messages")(capture_handler)
|
|
123
|
-
|
|
124
|
-
# Find available port
|
|
125
|
-
sock = socket.socket()
|
|
126
|
-
sock.bind(("", 0))
|
|
127
|
-
port = sock.getsockname()[1]
|
|
128
|
-
sock.close()
|
|
129
|
-
|
|
130
|
-
# Start server in background
|
|
131
|
-
from uvicorn import Config, Server
|
|
132
|
-
|
|
133
|
-
config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
|
|
134
|
-
server = Server(config)
|
|
135
|
-
|
|
136
|
-
server_task = asyncio.create_task(server.serve())
|
|
137
|
-
|
|
138
|
-
try:
|
|
139
|
-
# Wait for server to start
|
|
140
|
-
await asyncio.sleep(0.5)
|
|
141
|
-
|
|
142
|
-
# Execute Claude CLI with proxy
|
|
143
|
-
env = {**dict(os.environ), "ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}"}
|
|
144
|
-
|
|
145
|
-
process = await asyncio.create_subprocess_exec(
|
|
146
|
-
"claude",
|
|
147
|
-
"test",
|
|
148
|
-
env=env,
|
|
149
|
-
stdout=asyncio.subprocess.PIPE,
|
|
150
|
-
stderr=asyncio.subprocess.PIPE,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
# Wait for process with timeout
|
|
154
|
-
try:
|
|
155
|
-
await asyncio.wait_for(process.wait(), timeout=30)
|
|
156
|
-
except TimeoutError:
|
|
157
|
-
process.kill()
|
|
158
|
-
await process.wait()
|
|
159
|
-
|
|
160
|
-
# Stop server
|
|
161
|
-
server.should_exit = True
|
|
162
|
-
await server_task
|
|
163
|
-
|
|
164
|
-
if not captured_data:
|
|
165
|
-
raise RuntimeError("Failed to capture Claude CLI request")
|
|
166
|
-
|
|
167
|
-
# Extract headers and system prompt
|
|
168
|
-
headers = self._extract_headers(captured_data["headers"])
|
|
169
|
-
system_prompt = self._extract_system_prompt(captured_data["body"])
|
|
170
|
-
|
|
171
|
-
return ClaudeCacheData(
|
|
172
|
-
claude_version=version, headers=headers, system_prompt=system_prompt
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
except Exception as e:
|
|
176
|
-
# Ensure server is stopped
|
|
177
|
-
server.should_exit = True
|
|
178
|
-
if not server_task.done():
|
|
179
|
-
await server_task
|
|
180
|
-
raise
|
|
181
|
-
|
|
182
|
-
def _load_from_cache(self, version: str) -> ClaudeCacheData | None:
|
|
183
|
-
"""Load cached data for specific Claude version."""
|
|
184
|
-
cache_file = self.cache_dir / f"claude_headers_{version}.json"
|
|
185
|
-
|
|
186
|
-
if not cache_file.exists():
|
|
187
|
-
return None
|
|
188
|
-
|
|
189
|
-
try:
|
|
190
|
-
with cache_file.open("r") as f:
|
|
191
|
-
data = json.load(f)
|
|
192
|
-
return ClaudeCacheData.model_validate(data)
|
|
193
|
-
except Exception:
|
|
194
|
-
return None
|
|
195
|
-
|
|
196
|
-
def _save_to_cache(self, data: ClaudeCacheData) -> None:
|
|
197
|
-
"""Save detection data to cache."""
|
|
198
|
-
cache_file = self.cache_dir / f"claude_headers_{data.claude_version}.json"
|
|
199
|
-
|
|
200
|
-
try:
|
|
201
|
-
with cache_file.open("w") as f:
|
|
202
|
-
json.dump(data.model_dump(), f, indent=2, default=str)
|
|
203
|
-
logger.debug(
|
|
204
|
-
"cache_saved", file=str(cache_file), version=data.claude_version
|
|
205
|
-
)
|
|
206
|
-
except Exception as e:
|
|
207
|
-
logger.warning("cache_save_failed", file=str(cache_file), error=str(e))
|
|
208
|
-
|
|
209
|
-
def _extract_headers(self, headers: dict[str, str]) -> ClaudeCodeHeaders:
|
|
210
|
-
"""Extract Claude CLI headers from captured request."""
|
|
211
|
-
try:
|
|
212
|
-
return ClaudeCodeHeaders.model_validate(headers)
|
|
213
|
-
except Exception as e:
|
|
214
|
-
logger.error("header_extraction_failed", error=str(e))
|
|
215
|
-
raise ValueError(f"Failed to extract required headers: {e}") from e
|
|
216
|
-
|
|
217
|
-
def _extract_system_prompt(self, body: bytes) -> SystemPromptData:
|
|
218
|
-
"""Extract system prompt from captured request body."""
|
|
219
|
-
try:
|
|
220
|
-
data = json.loads(body.decode("utf-8"))
|
|
221
|
-
system_content = data.get("system")
|
|
222
|
-
|
|
223
|
-
if system_content is None:
|
|
224
|
-
raise ValueError("No system field found in request body")
|
|
225
|
-
|
|
226
|
-
return SystemPromptData(system_field=system_content)
|
|
227
|
-
|
|
228
|
-
except Exception as e:
|
|
229
|
-
logger.error("system_prompt_extraction_failed", error=str(e))
|
|
230
|
-
raise ValueError(f"Failed to extract system prompt: {e}") from e
|
|
231
|
-
|
|
232
|
-
def _get_fallback_data(self) -> ClaudeCacheData:
|
|
233
|
-
"""Get fallback data when detection fails."""
|
|
234
|
-
logger.warning("using_fallback_claude_data")
|
|
235
|
-
|
|
236
|
-
# Use existing hardcoded values as fallback
|
|
237
|
-
fallback_headers = ClaudeCodeHeaders(
|
|
238
|
-
**{
|
|
239
|
-
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
|
240
|
-
"anthropic-version": "2023-06-01",
|
|
241
|
-
"anthropic-dangerous-direct-browser-access": "true",
|
|
242
|
-
"x-app": "cli",
|
|
243
|
-
"User-Agent": "claude-cli/1.0.60 (external, cli)",
|
|
244
|
-
"X-Stainless-Lang": "js",
|
|
245
|
-
"X-Stainless-Retry-Count": "0",
|
|
246
|
-
"X-Stainless-Timeout": "60",
|
|
247
|
-
"X-Stainless-Package-Version": "0.55.1",
|
|
248
|
-
"X-Stainless-OS": "Linux",
|
|
249
|
-
"X-Stainless-Arch": "x64",
|
|
250
|
-
"X-Stainless-Runtime": "node",
|
|
251
|
-
"X-Stainless-Runtime-Version": "v24.3.0",
|
|
252
|
-
}
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
fallback_prompt = SystemPromptData(
|
|
256
|
-
system_field=[
|
|
257
|
-
{
|
|
258
|
-
"type": "text",
|
|
259
|
-
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
260
|
-
"cache_control": {"type": "ephemeral"},
|
|
261
|
-
}
|
|
262
|
-
]
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
return ClaudeCacheData(
|
|
266
|
-
claude_version="fallback",
|
|
267
|
-
headers=fallback_headers,
|
|
268
|
-
system_prompt=fallback_prompt,
|
|
269
|
-
)
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
"""Service for automatically detecting Codex CLI headers at startup."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import socket
|
|
9
|
-
import subprocess
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
import structlog
|
|
13
|
-
from fastapi import FastAPI, Request, Response
|
|
14
|
-
|
|
15
|
-
from ccproxy.config.discovery import get_ccproxy_cache_dir
|
|
16
|
-
from ccproxy.config.settings import Settings
|
|
17
|
-
from ccproxy.models.detection import (
|
|
18
|
-
CodexCacheData,
|
|
19
|
-
CodexHeaders,
|
|
20
|
-
CodexInstructionsData,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
logger = structlog.get_logger(__name__)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class CodexDetectionService:
|
|
28
|
-
"""Service for automatically detecting Codex CLI headers at startup."""
|
|
29
|
-
|
|
30
|
-
def __init__(self, settings: Settings) -> None:
|
|
31
|
-
"""Initialize Codex detection service."""
|
|
32
|
-
self.settings = settings
|
|
33
|
-
self.cache_dir = get_ccproxy_cache_dir()
|
|
34
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
-
self._cached_data: CodexCacheData | None = None
|
|
36
|
-
|
|
37
|
-
async def initialize_detection(self) -> CodexCacheData:
|
|
38
|
-
"""Initialize Codex detection at startup."""
|
|
39
|
-
try:
|
|
40
|
-
# Get current Codex version
|
|
41
|
-
current_version = await self._get_codex_version()
|
|
42
|
-
|
|
43
|
-
# Try to load from cache first
|
|
44
|
-
detected_data = self._load_from_cache(current_version)
|
|
45
|
-
cached = detected_data is not None
|
|
46
|
-
if cached:
|
|
47
|
-
logger.debug("detection_codex_headers_debug", version=current_version)
|
|
48
|
-
else:
|
|
49
|
-
# No cache or version changed - detect fresh
|
|
50
|
-
detected_data = await self._detect_codex_headers(current_version)
|
|
51
|
-
# Cache the results
|
|
52
|
-
self._save_to_cache(detected_data)
|
|
53
|
-
|
|
54
|
-
self._cached_data = detected_data
|
|
55
|
-
|
|
56
|
-
logger.info(
|
|
57
|
-
"detection_codex_headers_completed",
|
|
58
|
-
version=current_version,
|
|
59
|
-
cached=cached,
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# TODO: add proper testing without codex cli installed
|
|
63
|
-
if detected_data is None:
|
|
64
|
-
raise ValueError("Codex detection failed")
|
|
65
|
-
return detected_data
|
|
66
|
-
|
|
67
|
-
except Exception as e:
|
|
68
|
-
logger.warning("detection_codex_headers_failed", fallback=True, error=e)
|
|
69
|
-
# Return fallback data
|
|
70
|
-
fallback_data = self._get_fallback_data()
|
|
71
|
-
self._cached_data = fallback_data
|
|
72
|
-
return fallback_data
|
|
73
|
-
|
|
74
|
-
def get_cached_data(self) -> CodexCacheData | None:
|
|
75
|
-
"""Get currently cached detection data."""
|
|
76
|
-
return self._cached_data
|
|
77
|
-
|
|
78
|
-
async def _get_codex_version(self) -> str:
|
|
79
|
-
"""Get Codex CLI version."""
|
|
80
|
-
try:
|
|
81
|
-
result = subprocess.run(
|
|
82
|
-
["codex", "--version"],
|
|
83
|
-
capture_output=True,
|
|
84
|
-
text=True,
|
|
85
|
-
timeout=10,
|
|
86
|
-
)
|
|
87
|
-
if result.returncode == 0:
|
|
88
|
-
# Extract version from output like "codex 0.21.0"
|
|
89
|
-
version_line = result.stdout.strip()
|
|
90
|
-
if " " in version_line:
|
|
91
|
-
# Handle "codex 0.21.0" format - extract just the version number
|
|
92
|
-
return version_line.split()[-1]
|
|
93
|
-
return version_line
|
|
94
|
-
else:
|
|
95
|
-
raise RuntimeError(f"Codex version command failed: {result.stderr}")
|
|
96
|
-
|
|
97
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
|
|
98
|
-
logger.warning("codex_version_detection_failed", error=str(e))
|
|
99
|
-
return "unknown"
|
|
100
|
-
|
|
101
|
-
async def _detect_codex_headers(self, version: str) -> CodexCacheData:
|
|
102
|
-
"""Execute Codex CLI with proxy to capture headers and instructions."""
|
|
103
|
-
# Data captured from the request
|
|
104
|
-
captured_data: dict[str, Any] = {}
|
|
105
|
-
|
|
106
|
-
async def capture_handler(request: Request) -> Response:
|
|
107
|
-
"""Capture the Codex CLI request."""
|
|
108
|
-
captured_data["headers"] = dict(request.headers)
|
|
109
|
-
captured_data["body"] = await request.body()
|
|
110
|
-
# Return a mock response to satisfy Codex CLI
|
|
111
|
-
return Response(
|
|
112
|
-
content='{"choices": [{"message": {"content": "Test response"}}]}',
|
|
113
|
-
media_type="application/json",
|
|
114
|
-
status_code=200,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
# Create temporary FastAPI app
|
|
118
|
-
temp_app = FastAPI()
|
|
119
|
-
temp_app.post("/backend-api/codex/responses")(capture_handler)
|
|
120
|
-
|
|
121
|
-
# Find available port
|
|
122
|
-
sock = socket.socket()
|
|
123
|
-
sock.bind(("", 0))
|
|
124
|
-
port = sock.getsockname()[1]
|
|
125
|
-
sock.close()
|
|
126
|
-
|
|
127
|
-
# Start server in background
|
|
128
|
-
from uvicorn import Config, Server
|
|
129
|
-
|
|
130
|
-
config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
|
|
131
|
-
server = Server(config)
|
|
132
|
-
|
|
133
|
-
logger.debug("start")
|
|
134
|
-
server_task = asyncio.create_task(server.serve())
|
|
135
|
-
|
|
136
|
-
try:
|
|
137
|
-
# Wait for server to start
|
|
138
|
-
await asyncio.sleep(0.5)
|
|
139
|
-
|
|
140
|
-
# Execute Codex CLI with proxy
|
|
141
|
-
env = {
|
|
142
|
-
**dict(os.environ),
|
|
143
|
-
"OPENAI_BASE_URL": f"http://127.0.0.1:{port}/backend-api/codex",
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
process = await asyncio.create_subprocess_exec(
|
|
147
|
-
"codex",
|
|
148
|
-
"exec",
|
|
149
|
-
"test",
|
|
150
|
-
env=env,
|
|
151
|
-
stdout=asyncio.subprocess.PIPE,
|
|
152
|
-
stderr=asyncio.subprocess.PIPE,
|
|
153
|
-
)
|
|
154
|
-
# stderr = ""
|
|
155
|
-
# if process.stderr:
|
|
156
|
-
# stderr = await process.stderr.read(128)
|
|
157
|
-
# stdout = ""
|
|
158
|
-
# if process.stdout:
|
|
159
|
-
# stdout = await process.stdout.read(128)
|
|
160
|
-
# logger.warning("rcecdy", stderr=stderr, stdout=stdout)
|
|
161
|
-
|
|
162
|
-
# Wait for process with timeout
|
|
163
|
-
try:
|
|
164
|
-
await asyncio.wait_for(process.wait(), timeout=300)
|
|
165
|
-
except TimeoutError:
|
|
166
|
-
process.kill()
|
|
167
|
-
await process.wait()
|
|
168
|
-
|
|
169
|
-
# Stop server
|
|
170
|
-
server.should_exit = True
|
|
171
|
-
await server_task
|
|
172
|
-
|
|
173
|
-
if not captured_data:
|
|
174
|
-
raise RuntimeError("Failed to capture Codex CLI request")
|
|
175
|
-
|
|
176
|
-
# Extract headers and instructions
|
|
177
|
-
headers = self._extract_headers(captured_data["headers"])
|
|
178
|
-
instructions = self._extract_instructions(captured_data["body"])
|
|
179
|
-
|
|
180
|
-
return CodexCacheData(
|
|
181
|
-
codex_version=version, headers=headers, instructions=instructions
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
except Exception as e:
|
|
185
|
-
# Ensure server is stopped
|
|
186
|
-
server.should_exit = True
|
|
187
|
-
if not server_task.done():
|
|
188
|
-
await server_task
|
|
189
|
-
raise
|
|
190
|
-
|
|
191
|
-
def _load_from_cache(self, version: str) -> CodexCacheData | None:
|
|
192
|
-
"""Load cached data for specific Codex version."""
|
|
193
|
-
cache_file = self.cache_dir / f"codex_headers_{version}.json"
|
|
194
|
-
|
|
195
|
-
if not cache_file.exists():
|
|
196
|
-
return None
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
with cache_file.open("r") as f:
|
|
200
|
-
data = json.load(f)
|
|
201
|
-
return CodexCacheData.model_validate(data)
|
|
202
|
-
except Exception:
|
|
203
|
-
return None
|
|
204
|
-
|
|
205
|
-
def _save_to_cache(self, data: CodexCacheData) -> None:
|
|
206
|
-
"""Save detection data to cache."""
|
|
207
|
-
cache_file = self.cache_dir / f"codex_headers_{data.codex_version}.json"
|
|
208
|
-
|
|
209
|
-
try:
|
|
210
|
-
with cache_file.open("w") as f:
|
|
211
|
-
json.dump(data.model_dump(), f, indent=2, default=str)
|
|
212
|
-
logger.debug(
|
|
213
|
-
"cache_saved", file=str(cache_file), version=data.codex_version
|
|
214
|
-
)
|
|
215
|
-
except Exception as e:
|
|
216
|
-
logger.warning("cache_save_failed", file=str(cache_file), error=str(e))
|
|
217
|
-
|
|
218
|
-
def _extract_headers(self, headers: dict[str, str]) -> CodexHeaders:
|
|
219
|
-
"""Extract Codex CLI headers from captured request."""
|
|
220
|
-
try:
|
|
221
|
-
return CodexHeaders.model_validate(headers)
|
|
222
|
-
except Exception as e:
|
|
223
|
-
logger.error("header_extraction_failed", error=str(e))
|
|
224
|
-
raise ValueError(f"Failed to extract required headers: {e}") from e
|
|
225
|
-
|
|
226
|
-
def _extract_instructions(self, body: bytes) -> CodexInstructionsData:
|
|
227
|
-
"""Extract instructions from captured request body."""
|
|
228
|
-
try:
|
|
229
|
-
data = json.loads(body.decode("utf-8"))
|
|
230
|
-
instructions_content = data.get("instructions")
|
|
231
|
-
|
|
232
|
-
if instructions_content is None:
|
|
233
|
-
raise ValueError("No instructions field found in request body")
|
|
234
|
-
|
|
235
|
-
return CodexInstructionsData(instructions_field=instructions_content)
|
|
236
|
-
|
|
237
|
-
except Exception as e:
|
|
238
|
-
logger.error("instructions_extraction_failed", error=str(e))
|
|
239
|
-
raise ValueError(f"Failed to extract instructions: {e}") from e
|
|
240
|
-
|
|
241
|
-
def _get_fallback_data(self) -> CodexCacheData:
|
|
242
|
-
"""Get fallback data when detection fails."""
|
|
243
|
-
logger.warning("using_fallback_codex_data")
|
|
244
|
-
|
|
245
|
-
# Use hardcoded values as fallback from req.json
|
|
246
|
-
fallback_headers = CodexHeaders(
|
|
247
|
-
session_id="", # Will be generated per request
|
|
248
|
-
originator="codex_cli_rs",
|
|
249
|
-
**{"openai-beta": "responses=experimental"},
|
|
250
|
-
version="0.21.0",
|
|
251
|
-
**{"chatgpt-account-id": ""}, # Will be set from auth
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
# Use exact instructions from req.json
|
|
255
|
-
fallback_instructions = CodexInstructionsData(
|
|
256
|
-
instructions_field='You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.\n\nYour capabilities:\n- Receive user prompts and other context provided by the harness, such as files in the workspace.\n- Communicate with the user by streaming thinking & responses, and by making & updating plans.\n- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section.\n\nWithin this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI).'
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
return CodexCacheData(
|
|
260
|
-
codex_version="fallback",
|
|
261
|
-
headers=fallback_headers,
|
|
262
|
-
instructions=fallback_instructions,
|
|
263
|
-
)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""Credentials management package."""
|
|
2
|
-
|
|
3
|
-
from ccproxy.auth.exceptions import (
|
|
4
|
-
CredentialsError,
|
|
5
|
-
CredentialsExpiredError,
|
|
6
|
-
CredentialsInvalidError,
|
|
7
|
-
CredentialsNotFoundError,
|
|
8
|
-
CredentialsStorageError,
|
|
9
|
-
OAuthCallbackError,
|
|
10
|
-
OAuthError,
|
|
11
|
-
OAuthLoginError,
|
|
12
|
-
OAuthTokenRefreshError,
|
|
13
|
-
)
|
|
14
|
-
from ccproxy.auth.models import (
|
|
15
|
-
AccountInfo,
|
|
16
|
-
ClaudeCredentials,
|
|
17
|
-
OAuthToken,
|
|
18
|
-
OrganizationInfo,
|
|
19
|
-
UserProfile,
|
|
20
|
-
)
|
|
21
|
-
from ccproxy.auth.storage import JsonFileTokenStorage as JsonFileStorage
|
|
22
|
-
from ccproxy.auth.storage import TokenStorage as CredentialsStorageBackend
|
|
23
|
-
from ccproxy.services.credentials.config import CredentialsConfig, OAuthConfig
|
|
24
|
-
from ccproxy.services.credentials.manager import CredentialsManager
|
|
25
|
-
from ccproxy.services.credentials.oauth_client import OAuthClient
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
__all__ = [
|
|
29
|
-
# Manager
|
|
30
|
-
"CredentialsManager",
|
|
31
|
-
# Config
|
|
32
|
-
"CredentialsConfig",
|
|
33
|
-
"OAuthConfig",
|
|
34
|
-
# Models
|
|
35
|
-
"ClaudeCredentials",
|
|
36
|
-
"OAuthToken",
|
|
37
|
-
"OrganizationInfo",
|
|
38
|
-
"AccountInfo",
|
|
39
|
-
"UserProfile",
|
|
40
|
-
# Storage
|
|
41
|
-
"CredentialsStorageBackend",
|
|
42
|
-
"JsonFileStorage",
|
|
43
|
-
# OAuth
|
|
44
|
-
"OAuthClient",
|
|
45
|
-
# Exceptions
|
|
46
|
-
"CredentialsError",
|
|
47
|
-
"CredentialsNotFoundError",
|
|
48
|
-
"CredentialsInvalidError",
|
|
49
|
-
"CredentialsExpiredError",
|
|
50
|
-
"CredentialsStorageError",
|
|
51
|
-
"OAuthError",
|
|
52
|
-
"OAuthLoginError",
|
|
53
|
-
"OAuthTokenRefreshError",
|
|
54
|
-
"OAuthCallbackError",
|
|
55
|
-
]
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
"""Configuration for credentials and OAuth."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _get_default_storage_paths() -> list[str]:
|
|
9
|
-
"""Get default storage paths, with test override support."""
|
|
10
|
-
# Allow tests to override credential paths
|
|
11
|
-
if os.getenv("CCPROXY_TEST_MODE") == "true":
|
|
12
|
-
# Use a test-specific location that won't pollute real credentials
|
|
13
|
-
return [
|
|
14
|
-
"/tmp/ccproxy-test/.config/claude/.credentials.json",
|
|
15
|
-
"/tmp/ccproxy-test/.claude/.credentials.json",
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
return [
|
|
19
|
-
"~/.config/claude/.credentials.json", # Alternative legacy location
|
|
20
|
-
"~/.claude/.credentials.json", # Legacy location
|
|
21
|
-
"~/.config/ccproxy/credentials.json", # location in app config
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class OAuthConfig(BaseModel):
|
|
26
|
-
"""OAuth configuration settings."""
|
|
27
|
-
|
|
28
|
-
base_url: str = Field(
|
|
29
|
-
default="https://console.anthropic.com",
|
|
30
|
-
description="Base URL for OAuth API endpoints",
|
|
31
|
-
)
|
|
32
|
-
beta_version: str = Field(
|
|
33
|
-
default="oauth-2025-04-20",
|
|
34
|
-
description="OAuth beta version header",
|
|
35
|
-
)
|
|
36
|
-
token_url: str = Field(
|
|
37
|
-
default="https://console.anthropic.com/v1/oauth/token",
|
|
38
|
-
description="OAuth token endpoint URL",
|
|
39
|
-
)
|
|
40
|
-
authorize_url: str = Field(
|
|
41
|
-
default="https://claude.ai/oauth/authorize",
|
|
42
|
-
description="OAuth authorization endpoint URL",
|
|
43
|
-
)
|
|
44
|
-
profile_url: str = Field(
|
|
45
|
-
default="https://api.anthropic.com/api/oauth/profile",
|
|
46
|
-
description="OAuth profile endpoint URL",
|
|
47
|
-
)
|
|
48
|
-
client_id: str = Field(
|
|
49
|
-
default="9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
50
|
-
description="OAuth client ID",
|
|
51
|
-
)
|
|
52
|
-
redirect_uri: str = Field(
|
|
53
|
-
default="http://localhost:54545/callback",
|
|
54
|
-
description="OAuth redirect URI",
|
|
55
|
-
)
|
|
56
|
-
scopes: list[str] = Field(
|
|
57
|
-
default_factory=lambda: [
|
|
58
|
-
"org:create_api_key",
|
|
59
|
-
"user:profile",
|
|
60
|
-
"user:inference",
|
|
61
|
-
],
|
|
62
|
-
description="OAuth scopes to request",
|
|
63
|
-
)
|
|
64
|
-
request_timeout: int = Field(
|
|
65
|
-
default=30,
|
|
66
|
-
description="Timeout in seconds for OAuth requests",
|
|
67
|
-
)
|
|
68
|
-
user_agent: str = Field(
|
|
69
|
-
default="Claude-Code/1.0.43",
|
|
70
|
-
description="User agent string for OAuth requests",
|
|
71
|
-
)
|
|
72
|
-
callback_timeout: int = Field(
|
|
73
|
-
default=300,
|
|
74
|
-
description="Timeout in seconds for OAuth callback",
|
|
75
|
-
ge=60,
|
|
76
|
-
le=600,
|
|
77
|
-
)
|
|
78
|
-
callback_port: int = Field(
|
|
79
|
-
default=54545,
|
|
80
|
-
description="Port for OAuth callback server",
|
|
81
|
-
ge=1024,
|
|
82
|
-
le=65535,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class CredentialsConfig(BaseModel):
|
|
87
|
-
"""Configuration for credentials management."""
|
|
88
|
-
|
|
89
|
-
storage_paths: list[str] = Field(
|
|
90
|
-
default_factory=lambda: _get_default_storage_paths(),
|
|
91
|
-
description="Paths to search for credentials files",
|
|
92
|
-
)
|
|
93
|
-
oauth: OAuthConfig = Field(
|
|
94
|
-
default_factory=OAuthConfig,
|
|
95
|
-
description="OAuth configuration",
|
|
96
|
-
)
|
|
97
|
-
auto_refresh: bool = Field(
|
|
98
|
-
default=True,
|
|
99
|
-
description="Automatically refresh expired tokens",
|
|
100
|
-
)
|
|
101
|
-
refresh_buffer_seconds: int = Field(
|
|
102
|
-
default=300,
|
|
103
|
-
description="Refresh token this many seconds before expiry",
|
|
104
|
-
ge=0,
|
|
105
|
-
)
|