ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/config/validators.py
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
"""Configuration validation utilities."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
6
|
-
from urllib.parse import urlparse
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class ConfigValidationError(Exception):
|
|
10
|
-
"""Configuration validation error."""
|
|
11
|
-
|
|
12
|
-
pass
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def validate_host(host: str) -> str:
|
|
16
|
-
"""Validate host address.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
host: Host address to validate
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
The validated host address
|
|
23
|
-
|
|
24
|
-
Raises:
|
|
25
|
-
ConfigValidationError: If host is invalid
|
|
26
|
-
"""
|
|
27
|
-
if not host:
|
|
28
|
-
raise ConfigValidationError("Host cannot be empty")
|
|
29
|
-
|
|
30
|
-
# Allow localhost, IP addresses, and domain names
|
|
31
|
-
if host in ["localhost", "0.0.0.0", "127.0.0.1"]:
|
|
32
|
-
return host
|
|
33
|
-
|
|
34
|
-
# Basic IP address validation
|
|
35
|
-
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", host):
|
|
36
|
-
parts = host.split(".")
|
|
37
|
-
if all(0 <= int(part) <= 255 for part in parts):
|
|
38
|
-
return host
|
|
39
|
-
raise ConfigValidationError(f"Invalid IP address: {host}")
|
|
40
|
-
|
|
41
|
-
# Basic domain name validation
|
|
42
|
-
if re.match(r"^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", host):
|
|
43
|
-
return host
|
|
44
|
-
|
|
45
|
-
return host # Allow other formats for flexibility
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def validate_port(port: int | str) -> int:
|
|
49
|
-
"""Validate port number.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
port: Port number to validate
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
The validated port number
|
|
56
|
-
|
|
57
|
-
Raises:
|
|
58
|
-
ConfigValidationError: If port is invalid
|
|
59
|
-
"""
|
|
60
|
-
if isinstance(port, str):
|
|
61
|
-
try:
|
|
62
|
-
port = int(port)
|
|
63
|
-
except ValueError as e:
|
|
64
|
-
raise ConfigValidationError(f"Port must be a valid integer: {port}") from e
|
|
65
|
-
|
|
66
|
-
if not isinstance(port, int):
|
|
67
|
-
raise ConfigValidationError(f"Port must be an integer: {port}")
|
|
68
|
-
|
|
69
|
-
if port < 1 or port > 65535:
|
|
70
|
-
raise ConfigValidationError(f"Port must be between 1 and 65535: {port}")
|
|
71
|
-
|
|
72
|
-
return port
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def validate_url(url: str) -> str:
|
|
76
|
-
"""Validate URL format.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
url: URL to validate
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
The validated URL
|
|
83
|
-
|
|
84
|
-
Raises:
|
|
85
|
-
ConfigValidationError: If URL is invalid
|
|
86
|
-
"""
|
|
87
|
-
if not url:
|
|
88
|
-
raise ConfigValidationError("URL cannot be empty")
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
result = urlparse(url)
|
|
92
|
-
if not result.scheme or not result.netloc:
|
|
93
|
-
raise ConfigValidationError(f"Invalid URL format: {url}")
|
|
94
|
-
except Exception as e:
|
|
95
|
-
raise ConfigValidationError(f"Invalid URL: {url}") from e
|
|
96
|
-
|
|
97
|
-
return url
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def validate_path(path: str | Path) -> Path:
|
|
101
|
-
"""Validate file path.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
path: Path to validate
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
The validated Path object
|
|
108
|
-
|
|
109
|
-
Raises:
|
|
110
|
-
ConfigValidationError: If path is invalid
|
|
111
|
-
"""
|
|
112
|
-
if isinstance(path, str):
|
|
113
|
-
path = Path(path)
|
|
114
|
-
|
|
115
|
-
if not isinstance(path, Path):
|
|
116
|
-
raise ConfigValidationError(f"Path must be a string or Path object: {path}")
|
|
117
|
-
|
|
118
|
-
return path
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def validate_log_level(level: str) -> str:
|
|
122
|
-
"""Validate log level.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
level: Log level to validate
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
The validated log level
|
|
129
|
-
|
|
130
|
-
Raises:
|
|
131
|
-
ConfigValidationError: If log level is invalid
|
|
132
|
-
"""
|
|
133
|
-
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
134
|
-
level = level.upper()
|
|
135
|
-
|
|
136
|
-
if level not in valid_levels:
|
|
137
|
-
raise ConfigValidationError(
|
|
138
|
-
f"Invalid log level: {level}. Must be one of: {valid_levels}"
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
return level
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def validate_cors_origins(origins: list[str]) -> list[str]:
|
|
145
|
-
"""Validate CORS origins.
|
|
146
|
-
|
|
147
|
-
Args:
|
|
148
|
-
origins: List of origin URLs to validate
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
The validated list of origins
|
|
152
|
-
|
|
153
|
-
Raises:
|
|
154
|
-
ConfigValidationError: If any origin is invalid
|
|
155
|
-
"""
|
|
156
|
-
if not isinstance(origins, list):
|
|
157
|
-
raise ConfigValidationError("CORS origins must be a list")
|
|
158
|
-
|
|
159
|
-
validated_origins = []
|
|
160
|
-
for origin in origins:
|
|
161
|
-
if origin == "*":
|
|
162
|
-
validated_origins.append(origin)
|
|
163
|
-
else:
|
|
164
|
-
validated_origins.append(validate_url(origin))
|
|
165
|
-
|
|
166
|
-
return validated_origins
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def validate_timeout(timeout: int | float) -> int | float:
|
|
170
|
-
"""Validate timeout value.
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
timeout: Timeout value to validate
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
The validated timeout value
|
|
177
|
-
|
|
178
|
-
Raises:
|
|
179
|
-
ConfigValidationError: If timeout is invalid
|
|
180
|
-
"""
|
|
181
|
-
if not isinstance(timeout, int | float):
|
|
182
|
-
raise ConfigValidationError(f"Timeout must be a number: {timeout}")
|
|
183
|
-
|
|
184
|
-
if timeout <= 0:
|
|
185
|
-
raise ConfigValidationError(f"Timeout must be positive: {timeout}")
|
|
186
|
-
|
|
187
|
-
return timeout
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def validate_config_dict(config: dict[str, Any]) -> dict[str, Any]:
|
|
191
|
-
"""Validate configuration dictionary.
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
config: Configuration dictionary to validate
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
The validated configuration dictionary
|
|
198
|
-
|
|
199
|
-
Raises:
|
|
200
|
-
ConfigValidationError: If configuration is invalid
|
|
201
|
-
"""
|
|
202
|
-
if not isinstance(config, dict):
|
|
203
|
-
raise ConfigValidationError("Configuration must be a dictionary")
|
|
204
|
-
|
|
205
|
-
validated_config: dict[str, Any] = {}
|
|
206
|
-
|
|
207
|
-
# Validate specific fields if present
|
|
208
|
-
if "host" in config:
|
|
209
|
-
validated_config["host"] = validate_host(config["host"])
|
|
210
|
-
|
|
211
|
-
if "port" in config:
|
|
212
|
-
validated_config["port"] = validate_port(config["port"])
|
|
213
|
-
|
|
214
|
-
if "target_url" in config:
|
|
215
|
-
validated_config["target_url"] = validate_url(config["target_url"])
|
|
216
|
-
|
|
217
|
-
if "log_level" in config:
|
|
218
|
-
validated_config["log_level"] = validate_log_level(config["log_level"])
|
|
219
|
-
|
|
220
|
-
if "cors_origins" in config:
|
|
221
|
-
validated_config["cors_origins"] = validate_cors_origins(config["cors_origins"])
|
|
222
|
-
|
|
223
|
-
if "timeout" in config:
|
|
224
|
-
validated_config["timeout"] = validate_timeout(config["timeout"])
|
|
225
|
-
|
|
226
|
-
# Copy other fields without validation
|
|
227
|
-
for key, value in config.items():
|
|
228
|
-
if key not in validated_config:
|
|
229
|
-
validated_config[key] = value
|
|
230
|
-
|
|
231
|
-
return validated_config
|
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
"""Codex-specific transformers for request/response transformation."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
import structlog
|
|
6
|
-
from typing_extensions import TypedDict
|
|
7
|
-
|
|
8
|
-
from ccproxy.core.transformers import RequestTransformer
|
|
9
|
-
from ccproxy.core.types import ProxyRequest, TransformContext
|
|
10
|
-
from ccproxy.models.detection import CodexCacheData
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
logger = structlog.get_logger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class CodexRequestData(TypedDict):
|
|
17
|
-
"""Typed structure for transformed Codex request data."""
|
|
18
|
-
|
|
19
|
-
method: str
|
|
20
|
-
url: str
|
|
21
|
-
headers: dict[str, str]
|
|
22
|
-
body: bytes | None
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class CodexRequestTransformer(RequestTransformer):
|
|
26
|
-
"""Codex request transformer for header and instructions field injection."""
|
|
27
|
-
|
|
28
|
-
def __init__(self) -> None:
|
|
29
|
-
"""Initialize Codex request transformer."""
|
|
30
|
-
super().__init__()
|
|
31
|
-
|
|
32
|
-
async def _transform_request(
|
|
33
|
-
self, request: ProxyRequest, context: TransformContext | None = None
|
|
34
|
-
) -> ProxyRequest:
|
|
35
|
-
"""Transform a proxy request for Codex API.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
request: The structured proxy request to transform
|
|
39
|
-
context: Optional transformation context
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
The transformed proxy request
|
|
43
|
-
"""
|
|
44
|
-
# Extract required data from context
|
|
45
|
-
access_token = ""
|
|
46
|
-
session_id = ""
|
|
47
|
-
account_id = ""
|
|
48
|
-
codex_detection_data = None
|
|
49
|
-
|
|
50
|
-
if context:
|
|
51
|
-
if hasattr(context, "access_token"):
|
|
52
|
-
access_token = context.access_token
|
|
53
|
-
elif isinstance(context, dict):
|
|
54
|
-
access_token = context.get("access_token", "")
|
|
55
|
-
|
|
56
|
-
if hasattr(context, "session_id"):
|
|
57
|
-
session_id = context.session_id
|
|
58
|
-
elif isinstance(context, dict):
|
|
59
|
-
session_id = context.get("session_id", "")
|
|
60
|
-
|
|
61
|
-
if hasattr(context, "account_id"):
|
|
62
|
-
account_id = context.account_id
|
|
63
|
-
elif isinstance(context, dict):
|
|
64
|
-
account_id = context.get("account_id", "")
|
|
65
|
-
|
|
66
|
-
if hasattr(context, "codex_detection_data"):
|
|
67
|
-
codex_detection_data = context.codex_detection_data
|
|
68
|
-
elif isinstance(context, dict):
|
|
69
|
-
codex_detection_data = context.get("codex_detection_data")
|
|
70
|
-
|
|
71
|
-
# Transform URL - remove codex prefix and forward to ChatGPT backend
|
|
72
|
-
transformed_url = self._transform_codex_url(request.url)
|
|
73
|
-
|
|
74
|
-
# Convert request body to bytes for header processing
|
|
75
|
-
body_bytes = None
|
|
76
|
-
if request.body:
|
|
77
|
-
if isinstance(request.body, bytes):
|
|
78
|
-
body_bytes = request.body
|
|
79
|
-
elif isinstance(request.body, str):
|
|
80
|
-
body_bytes = request.body.encode("utf-8")
|
|
81
|
-
elif isinstance(request.body, dict):
|
|
82
|
-
body_bytes = json.dumps(request.body).encode("utf-8")
|
|
83
|
-
|
|
84
|
-
# Transform headers with Codex CLI identity
|
|
85
|
-
transformed_headers = self.create_codex_headers(
|
|
86
|
-
request.headers,
|
|
87
|
-
access_token,
|
|
88
|
-
session_id,
|
|
89
|
-
account_id,
|
|
90
|
-
body_bytes,
|
|
91
|
-
codex_detection_data,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
# Transform body to inject instructions
|
|
95
|
-
transformed_body = request.body
|
|
96
|
-
if request.body:
|
|
97
|
-
if isinstance(request.body, bytes):
|
|
98
|
-
transformed_body = self.transform_codex_body(
|
|
99
|
-
request.body, codex_detection_data
|
|
100
|
-
)
|
|
101
|
-
else:
|
|
102
|
-
# Convert to bytes if needed
|
|
103
|
-
body_bytes = (
|
|
104
|
-
json.dumps(request.body).encode("utf-8")
|
|
105
|
-
if isinstance(request.body, dict)
|
|
106
|
-
else str(request.body).encode("utf-8")
|
|
107
|
-
)
|
|
108
|
-
transformed_body = self.transform_codex_body(
|
|
109
|
-
body_bytes, codex_detection_data
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
# Create new transformed request
|
|
113
|
-
return ProxyRequest(
|
|
114
|
-
method=request.method,
|
|
115
|
-
url=transformed_url,
|
|
116
|
-
headers=transformed_headers,
|
|
117
|
-
params={}, # Query params handled in URL
|
|
118
|
-
body=transformed_body,
|
|
119
|
-
protocol=request.protocol,
|
|
120
|
-
timeout=request.timeout,
|
|
121
|
-
metadata=request.metadata,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
async def transform_codex_request(
|
|
125
|
-
self,
|
|
126
|
-
method: str,
|
|
127
|
-
path: str,
|
|
128
|
-
headers: dict[str, str],
|
|
129
|
-
body: bytes | None,
|
|
130
|
-
access_token: str,
|
|
131
|
-
session_id: str,
|
|
132
|
-
account_id: str,
|
|
133
|
-
codex_detection_data: CodexCacheData | None = None,
|
|
134
|
-
target_base_url: str = "https://chatgpt.com/backend-api/codex",
|
|
135
|
-
) -> CodexRequestData:
|
|
136
|
-
"""Transform Codex request using direct parameters from ProxyService.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
method: HTTP method
|
|
140
|
-
path: Request path
|
|
141
|
-
headers: Request headers
|
|
142
|
-
body: Request body
|
|
143
|
-
access_token: OAuth access token
|
|
144
|
-
session_id: Codex session ID
|
|
145
|
-
account_id: ChatGPT account ID
|
|
146
|
-
codex_detection_data: Optional Codex detection data
|
|
147
|
-
target_base_url: Base URL for the Codex API
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
Dictionary with transformed request data (method, url, headers, body)
|
|
151
|
-
"""
|
|
152
|
-
# Transform URL path
|
|
153
|
-
transformed_path = self._transform_codex_path(path)
|
|
154
|
-
target_url = f"{target_base_url.rstrip('/')}{transformed_path}"
|
|
155
|
-
|
|
156
|
-
# Transform body first (inject instructions)
|
|
157
|
-
codex_body = None
|
|
158
|
-
if body:
|
|
159
|
-
# body is guaranteed to be bytes due to parameter type
|
|
160
|
-
codex_body = self.transform_codex_body(body, codex_detection_data)
|
|
161
|
-
|
|
162
|
-
# Transform headers with Codex CLI identity and authentication
|
|
163
|
-
codex_headers = self.create_codex_headers(
|
|
164
|
-
headers, access_token, session_id, account_id, body, codex_detection_data
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
# Update Content-Length if body was transformed and size changed
|
|
168
|
-
if codex_body and body and len(codex_body) != len(body):
|
|
169
|
-
# Remove any existing content-length headers (case-insensitive)
|
|
170
|
-
codex_headers = {
|
|
171
|
-
k: v for k, v in codex_headers.items() if k.lower() != "content-length"
|
|
172
|
-
}
|
|
173
|
-
codex_headers["Content-Length"] = str(len(codex_body))
|
|
174
|
-
elif codex_body and not body:
|
|
175
|
-
# New body was created where none existed
|
|
176
|
-
codex_headers["Content-Length"] = str(len(codex_body))
|
|
177
|
-
|
|
178
|
-
return CodexRequestData(
|
|
179
|
-
method=method,
|
|
180
|
-
url=target_url,
|
|
181
|
-
headers=codex_headers,
|
|
182
|
-
body=codex_body,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
def _transform_codex_url(self, url: str) -> str:
|
|
186
|
-
"""Transform URL from proxy format to ChatGPT backend format."""
|
|
187
|
-
# Extract base URL and path
|
|
188
|
-
if "://" in url:
|
|
189
|
-
protocol, rest = url.split("://", 1)
|
|
190
|
-
if "/" in rest:
|
|
191
|
-
domain, path = rest.split("/", 1)
|
|
192
|
-
path = "/" + path
|
|
193
|
-
else:
|
|
194
|
-
path = "/"
|
|
195
|
-
else:
|
|
196
|
-
path = url if url.startswith("/") else "/" + url
|
|
197
|
-
|
|
198
|
-
# Transform path and build target URL
|
|
199
|
-
transformed_path = self._transform_codex_path(path)
|
|
200
|
-
return f"https://chatgpt.com/backend-api/codex{transformed_path}"
|
|
201
|
-
|
|
202
|
-
def _transform_codex_path(self, path: str) -> str:
|
|
203
|
-
"""Transform request path for Codex API."""
|
|
204
|
-
# Remove /codex prefix if present
|
|
205
|
-
if path.startswith("/codex"):
|
|
206
|
-
path = path[6:] # Remove "/codex" prefix
|
|
207
|
-
|
|
208
|
-
# Ensure we have a valid path
|
|
209
|
-
if not path or path == "/":
|
|
210
|
-
path = "/responses"
|
|
211
|
-
|
|
212
|
-
# Handle session_id in path for /codex/{session_id}/responses pattern
|
|
213
|
-
if path.startswith("/") and "/" in path[1:]:
|
|
214
|
-
# This might be /{session_id}/responses - extract the responses part
|
|
215
|
-
parts = path.strip("/").split("/")
|
|
216
|
-
if len(parts) >= 2 and parts[-1] == "responses":
|
|
217
|
-
# Keep the /responses endpoint, session_id will be in headers
|
|
218
|
-
path = "/responses"
|
|
219
|
-
|
|
220
|
-
return path
|
|
221
|
-
|
|
222
|
-
def create_codex_headers(
|
|
223
|
-
self,
|
|
224
|
-
headers: dict[str, str],
|
|
225
|
-
access_token: str,
|
|
226
|
-
session_id: str,
|
|
227
|
-
account_id: str,
|
|
228
|
-
body: bytes | None = None,
|
|
229
|
-
codex_detection_data: CodexCacheData | None = None,
|
|
230
|
-
) -> dict[str, str]:
|
|
231
|
-
"""Create Codex headers with CLI identity and authentication."""
|
|
232
|
-
codex_headers = {}
|
|
233
|
-
|
|
234
|
-
# Strip potentially problematic headers
|
|
235
|
-
excluded_headers = {
|
|
236
|
-
"host",
|
|
237
|
-
"x-forwarded-for",
|
|
238
|
-
"x-forwarded-proto",
|
|
239
|
-
"x-forwarded-host",
|
|
240
|
-
"forwarded",
|
|
241
|
-
# Authentication headers to be replaced
|
|
242
|
-
"authorization",
|
|
243
|
-
"x-api-key",
|
|
244
|
-
# Compression headers to avoid decompression issues
|
|
245
|
-
"accept-encoding",
|
|
246
|
-
"content-encoding",
|
|
247
|
-
# CORS headers - should not be forwarded to upstream
|
|
248
|
-
"origin",
|
|
249
|
-
"access-control-request-method",
|
|
250
|
-
"access-control-request-headers",
|
|
251
|
-
"access-control-allow-origin",
|
|
252
|
-
"access-control-allow-methods",
|
|
253
|
-
"access-control-allow-headers",
|
|
254
|
-
"access-control-allow-credentials",
|
|
255
|
-
"access-control-max-age",
|
|
256
|
-
"access-control-expose-headers",
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
# Copy important headers (excluding problematic ones)
|
|
260
|
-
for key, value in headers.items():
|
|
261
|
-
lower_key = key.lower()
|
|
262
|
-
if lower_key not in excluded_headers:
|
|
263
|
-
codex_headers[key] = value
|
|
264
|
-
|
|
265
|
-
# Set authentication with OAuth token
|
|
266
|
-
if access_token:
|
|
267
|
-
codex_headers["Authorization"] = f"Bearer {access_token}"
|
|
268
|
-
|
|
269
|
-
# Set defaults for essential headers
|
|
270
|
-
if "content-type" not in [k.lower() for k in codex_headers]:
|
|
271
|
-
codex_headers["Content-Type"] = "application/json"
|
|
272
|
-
if "accept" not in [k.lower() for k in codex_headers]:
|
|
273
|
-
codex_headers["Accept"] = "application/json"
|
|
274
|
-
|
|
275
|
-
# Use detected Codex CLI headers when available
|
|
276
|
-
if codex_detection_data:
|
|
277
|
-
detected_headers = codex_detection_data.headers.to_headers_dict()
|
|
278
|
-
# Override with session-specific values
|
|
279
|
-
detected_headers["session_id"] = session_id
|
|
280
|
-
if account_id:
|
|
281
|
-
detected_headers["chatgpt-account-id"] = account_id
|
|
282
|
-
codex_headers.update(detected_headers)
|
|
283
|
-
logger.debug(
|
|
284
|
-
"using_detected_codex_headers",
|
|
285
|
-
version=codex_detection_data.codex_version,
|
|
286
|
-
)
|
|
287
|
-
else:
|
|
288
|
-
# Fallback to hardcoded Codex headers
|
|
289
|
-
codex_headers.update(
|
|
290
|
-
{
|
|
291
|
-
"session_id": session_id,
|
|
292
|
-
"originator": "codex_cli_rs",
|
|
293
|
-
"openai-beta": "responses=experimental",
|
|
294
|
-
"version": "0.21.0",
|
|
295
|
-
}
|
|
296
|
-
)
|
|
297
|
-
if account_id:
|
|
298
|
-
codex_headers["chatgpt-account-id"] = account_id
|
|
299
|
-
logger.debug("using_fallback_codex_headers")
|
|
300
|
-
|
|
301
|
-
# Don't set Accept header - let the backend handle it based on stream parameter
|
|
302
|
-
# Setting Accept: text/event-stream with stream:true in body causes 400 Bad Request
|
|
303
|
-
# The backend will determine the response format based on the stream parameter
|
|
304
|
-
|
|
305
|
-
return codex_headers
|
|
306
|
-
|
|
307
|
-
def _is_streaming_request(self, body: bytes | None) -> bool:
|
|
308
|
-
"""Check if the request body indicates a streaming request (including injected default)."""
|
|
309
|
-
if not body:
|
|
310
|
-
return False
|
|
311
|
-
|
|
312
|
-
try:
|
|
313
|
-
data = json.loads(body.decode("utf-8"))
|
|
314
|
-
return data.get("stream", False) is True
|
|
315
|
-
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
316
|
-
return False
|
|
317
|
-
|
|
318
|
-
def _is_user_streaming_request(self, body: bytes | None) -> bool:
|
|
319
|
-
"""Check if the user explicitly requested streaming (has 'stream' field in original body)."""
|
|
320
|
-
if not body:
|
|
321
|
-
return False
|
|
322
|
-
|
|
323
|
-
try:
|
|
324
|
-
data = json.loads(body.decode("utf-8"))
|
|
325
|
-
# Only return True if user explicitly included "stream" field (regardless of its value)
|
|
326
|
-
return "stream" in data and data.get("stream") is True
|
|
327
|
-
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
328
|
-
return False
|
|
329
|
-
|
|
330
|
-
def transform_codex_body(
|
|
331
|
-
self, body: bytes, codex_detection_data: CodexCacheData | None = None
|
|
332
|
-
) -> bytes:
|
|
333
|
-
"""Transform request body to inject Codex CLI instructions."""
|
|
334
|
-
if not body:
|
|
335
|
-
return body
|
|
336
|
-
|
|
337
|
-
try:
|
|
338
|
-
data = json.loads(body.decode("utf-8"))
|
|
339
|
-
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
340
|
-
# Return original if not valid JSON
|
|
341
|
-
logger.warning(
|
|
342
|
-
"codex_transform_json_decode_failed",
|
|
343
|
-
error=str(e),
|
|
344
|
-
body_preview=body[:200].decode("utf-8", errors="replace")
|
|
345
|
-
if body
|
|
346
|
-
else None,
|
|
347
|
-
body_length=len(body) if body else 0,
|
|
348
|
-
)
|
|
349
|
-
return body
|
|
350
|
-
|
|
351
|
-
# Check if this request already has the full Codex instructions
|
|
352
|
-
# If instructions field exists and is longer than 1000 chars, it's already set
|
|
353
|
-
if (
|
|
354
|
-
"instructions" in data
|
|
355
|
-
and data["instructions"]
|
|
356
|
-
and len(data["instructions"]) > 1000
|
|
357
|
-
):
|
|
358
|
-
# This already has full Codex instructions, don't replace them
|
|
359
|
-
logger.debug("skipping_codex_transform_has_full_instructions")
|
|
360
|
-
return body
|
|
361
|
-
|
|
362
|
-
# Get the instructions to inject
|
|
363
|
-
detected_instructions = None
|
|
364
|
-
if codex_detection_data:
|
|
365
|
-
detected_instructions = codex_detection_data.instructions.instructions_field
|
|
366
|
-
else:
|
|
367
|
-
# Fallback instructions from req.json
|
|
368
|
-
detected_instructions = (
|
|
369
|
-
"You are a coding agent running in the Codex CLI, a terminal-based coding assistant. "
|
|
370
|
-
"Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.\n\n"
|
|
371
|
-
"Your capabilities:\n"
|
|
372
|
-
"- Receive user prompts and other context provided by the harness, such as files in the workspace.\n"
|
|
373
|
-
"- Communicate with the user by streaming thinking & responses, and by making & updating plans.\n"
|
|
374
|
-
"- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, "
|
|
375
|
-
"you can request that these function calls be escalated to the user for approval before running. "
|
|
376
|
-
'More on this in the "Sandbox and approvals" section.\n\n'
|
|
377
|
-
"Within this context, Codex refers to the open-source agentic coding interface "
|
|
378
|
-
"(not the old Codex language model built by OpenAI)."
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
# Always inject/override the instructions field
|
|
382
|
-
data["instructions"] = detected_instructions
|
|
383
|
-
|
|
384
|
-
# Only inject stream: true if user explicitly requested streaming or didn't specify
|
|
385
|
-
# For now, we'll inject stream: true by default since Codex seems to expect it
|
|
386
|
-
if "stream" not in data:
|
|
387
|
-
data["stream"] = True
|
|
388
|
-
|
|
389
|
-
return json.dumps(data, separators=(",", ":")).encode("utf-8")
|