ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Codex plugin-specific configuration settings."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
2
4
|
|
|
3
5
|
from pydantic import BaseModel, Field, field_validator
|
|
4
6
|
|
|
7
|
+
from ccproxy.core.constants import (
|
|
8
|
+
FORMAT_ANTHROPIC_MESSAGES,
|
|
9
|
+
FORMAT_OPENAI_CHAT,
|
|
10
|
+
FORMAT_OPENAI_RESPONSES,
|
|
11
|
+
)
|
|
12
|
+
from ccproxy.models.provider import ModelCard, ModelMappingRule, ProviderConfig
|
|
13
|
+
from ccproxy.plugins.codex.model_defaults import (
|
|
14
|
+
DEFAULT_CODEX_MODEL_CARDS,
|
|
15
|
+
DEFAULT_CODEX_MODEL_MAPPINGS,
|
|
16
|
+
)
|
|
17
|
+
|
|
5
18
|
|
|
6
19
|
class OAuthSettings(BaseModel):
|
|
7
20
|
"""OAuth configuration for OpenAI authentication."""
|
|
@@ -30,19 +43,12 @@ class OAuthSettings(BaseModel):
|
|
|
30
43
|
return v.rstrip("/")
|
|
31
44
|
|
|
32
45
|
|
|
33
|
-
class CodexSettings(
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
enabled: bool = Field(
|
|
37
|
-
default=True,
|
|
38
|
-
description="Enable OpenAI Codex provider support",
|
|
39
|
-
)
|
|
46
|
+
class CodexSettings(ProviderConfig):
|
|
47
|
+
"""Codex plugin configuration extending base ProviderConfig."""
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
default="https://chatgpt.com/backend-api/codex",
|
|
43
|
-
description="OpenAI Codex API base URL",
|
|
44
|
-
)
|
|
49
|
+
# Base ProviderConfig fields will be inherited
|
|
45
50
|
|
|
51
|
+
# Codex-specific OAuth settings
|
|
46
52
|
oauth: OAuthSettings = Field(
|
|
47
53
|
default_factory=OAuthSettings,
|
|
48
54
|
description="OAuth configuration settings",
|
|
@@ -65,6 +71,66 @@ class CodexSettings(BaseModel):
|
|
|
65
71
|
description="Enable verbose logging for Codex operations",
|
|
66
72
|
)
|
|
67
73
|
|
|
74
|
+
# NEW: Auth manager override support
|
|
75
|
+
auth_manager: str | None = Field(
|
|
76
|
+
default=None,
|
|
77
|
+
description="Override auth manager name (e.g., 'oauth_codex_lb' for load balancing)",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Override base_url default for Codex
|
|
81
|
+
base_url: str = Field(
|
|
82
|
+
default="https://chatgpt.com/backend-api/codex",
|
|
83
|
+
description="OpenAI Codex API base URL",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Set defaults for inherited fields
|
|
87
|
+
name: str = Field(default="codex", description="Provider name")
|
|
88
|
+
supports_streaming: bool = Field(
|
|
89
|
+
default=True, description="Whether the provider supports streaming"
|
|
90
|
+
)
|
|
91
|
+
requires_auth: bool = Field(
|
|
92
|
+
default=True, description="Whether the provider requires authentication"
|
|
93
|
+
)
|
|
94
|
+
auth_type: str | None = Field(
|
|
95
|
+
default="oauth", description="Authentication type (bearer, api_key, etc.)"
|
|
96
|
+
)
|
|
97
|
+
model_mappings: list[ModelMappingRule] = Field(
|
|
98
|
+
default_factory=lambda: [
|
|
99
|
+
rule.model_copy(deep=True) for rule in DEFAULT_CODEX_MODEL_MAPPINGS
|
|
100
|
+
],
|
|
101
|
+
description="List of client-to-upstream model mapping rules",
|
|
102
|
+
)
|
|
103
|
+
models_endpoint: list[ModelCard] = Field(
|
|
104
|
+
default_factory=lambda: [
|
|
105
|
+
card.model_copy(deep=True) for card in DEFAULT_CODEX_MODEL_CARDS
|
|
106
|
+
],
|
|
107
|
+
description="Model metadata served via the /models endpoint",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
supported_input_formats: list[str] = Field(
|
|
111
|
+
default_factory=lambda: [
|
|
112
|
+
FORMAT_OPENAI_RESPONSES,
|
|
113
|
+
FORMAT_OPENAI_CHAT,
|
|
114
|
+
FORMAT_ANTHROPIC_MESSAGES,
|
|
115
|
+
],
|
|
116
|
+
description="List of supported input formats",
|
|
117
|
+
)
|
|
118
|
+
preferred_upstream_mode: Literal["streaming", "non_streaming"] = Field(
|
|
119
|
+
default="streaming", description="Preferred upstream mode for requests"
|
|
120
|
+
)
|
|
121
|
+
buffer_non_streaming: bool = Field(
|
|
122
|
+
default=True, description="Whether to buffer non-streaming requests"
|
|
123
|
+
)
|
|
124
|
+
enable_format_registry: bool = Field(
|
|
125
|
+
default=True, description="Whether to enable format adapter registry"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Detection configuration
|
|
129
|
+
detection_home_mode: Literal["temp", "home"] = Field(
|
|
130
|
+
default="temp",
|
|
131
|
+
description="Home directory mode for CLI detection: 'temp' uses temporary directory, 'home' uses actual user HOME",
|
|
132
|
+
)
|
|
133
|
+
|
|
68
134
|
@field_validator("base_url")
|
|
69
135
|
@classmethod
|
|
70
136
|
def validate_base_url(cls, v: str) -> str:
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""Service for detecting Codex CLI using centralized detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import socket
|
|
9
|
+
import tempfile
|
|
10
|
+
from contextlib import suppress
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, cast
|
|
13
|
+
|
|
14
|
+
from fastapi import FastAPI, Request, Response
|
|
15
|
+
|
|
16
|
+
from ccproxy.config.settings import Settings
|
|
17
|
+
from ccproxy.config.utils import get_ccproxy_cache_dir
|
|
18
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
19
|
+
from ccproxy.models.detection import DetectedHeaders, DetectedPrompts
|
|
20
|
+
from ccproxy.services.cli_detection import CLIDetectionService
|
|
21
|
+
from ccproxy.utils.caching import async_ttl_cache
|
|
22
|
+
from ccproxy.utils.headers import extract_request_headers
|
|
23
|
+
|
|
24
|
+
from .config import CodexSettings
|
|
25
|
+
from .models import CodexCacheData, CodexCliInfo
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
logger = get_plugin_logger()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CodexDetectionService:
|
|
32
|
+
"""Service for automatically detecting Codex CLI headers at startup."""
|
|
33
|
+
|
|
34
|
+
# Headers whose values are redacted in cache (lowercase)
|
|
35
|
+
REDACTED_HEADERS = [
|
|
36
|
+
"authorization",
|
|
37
|
+
"session_id",
|
|
38
|
+
"conversation_id",
|
|
39
|
+
"chatgpt-account-id",
|
|
40
|
+
"host",
|
|
41
|
+
]
|
|
42
|
+
# Headers to ignore at injection time (lowercase). Cache retains keys with empty values to preserve order.
|
|
43
|
+
ignores_header: list[str] = [
|
|
44
|
+
"host",
|
|
45
|
+
"content-length",
|
|
46
|
+
"authorization",
|
|
47
|
+
"x-api-key",
|
|
48
|
+
"session_id",
|
|
49
|
+
"conversation_id",
|
|
50
|
+
"chatgpt-account-id",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
settings: Settings,
|
|
56
|
+
cli_service: CLIDetectionService | None = None,
|
|
57
|
+
codex_settings: CodexSettings | None = None,
|
|
58
|
+
redact_sensitive_cache: bool = True,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Initialize Codex detection service.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
settings: Application settings
|
|
64
|
+
cli_service: Optional CLI detection service for dependency injection.
|
|
65
|
+
If None, creates its own instance.
|
|
66
|
+
codex_settings: Optional Codex plugin settings for plugin-specific configuration.
|
|
67
|
+
If None, uses default configuration.
|
|
68
|
+
"""
|
|
69
|
+
self.settings = settings
|
|
70
|
+
self.codex_settings = codex_settings if codex_settings else CodexSettings()
|
|
71
|
+
self.cache_dir = get_ccproxy_cache_dir()
|
|
72
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
self._cached_data: CodexCacheData | None = None
|
|
74
|
+
self._cli_service = cli_service or CLIDetectionService(settings)
|
|
75
|
+
self._cli_info: CodexCliInfo | None = None
|
|
76
|
+
self._redact_sensitive_cache = redact_sensitive_cache
|
|
77
|
+
|
|
78
|
+
async def initialize_detection(self) -> CodexCacheData:
|
|
79
|
+
"""Initialize Codex detection at startup."""
|
|
80
|
+
try:
|
|
81
|
+
# Get current Codex version
|
|
82
|
+
current_version = await self._get_codex_version()
|
|
83
|
+
|
|
84
|
+
detected_data = None
|
|
85
|
+
# Try to load from cache first
|
|
86
|
+
cached = False
|
|
87
|
+
try:
|
|
88
|
+
detected_data = self._load_from_cache(current_version)
|
|
89
|
+
cached = detected_data is not None
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.warning(
|
|
92
|
+
"invalid_cache_file",
|
|
93
|
+
error=str(e),
|
|
94
|
+
category="plugin",
|
|
95
|
+
exc_info=e,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if not cached:
|
|
99
|
+
# No cache or version changed - detect fresh
|
|
100
|
+
detected_data = await self._detect_codex_headers(current_version)
|
|
101
|
+
# Cache the results
|
|
102
|
+
self._save_to_cache(detected_data)
|
|
103
|
+
|
|
104
|
+
self._cached_data = detected_data
|
|
105
|
+
|
|
106
|
+
logger.trace(
|
|
107
|
+
"detection_headers_completed",
|
|
108
|
+
version=current_version,
|
|
109
|
+
cached=cached,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if detected_data is None:
|
|
113
|
+
raise ValueError("Codex detection failed")
|
|
114
|
+
return detected_data
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.warning(
|
|
118
|
+
"detection_codex_headers_failed",
|
|
119
|
+
fallback=True,
|
|
120
|
+
exc_info=e,
|
|
121
|
+
category="plugin",
|
|
122
|
+
)
|
|
123
|
+
# Return fallback data
|
|
124
|
+
fallback_data = self._get_fallback_data()
|
|
125
|
+
self._cached_data = fallback_data
|
|
126
|
+
return fallback_data
|
|
127
|
+
|
|
128
|
+
def get_cached_data(self) -> CodexCacheData | None:
|
|
129
|
+
"""Get currently cached detection data."""
|
|
130
|
+
return self._cached_data
|
|
131
|
+
|
|
132
|
+
def get_detected_headers(self) -> DetectedHeaders:
|
|
133
|
+
"""Return cached headers as structured data."""
|
|
134
|
+
|
|
135
|
+
data = self.get_cached_data()
|
|
136
|
+
if not data:
|
|
137
|
+
return DetectedHeaders()
|
|
138
|
+
return data.headers
|
|
139
|
+
|
|
140
|
+
def get_detected_prompts(self) -> DetectedPrompts:
|
|
141
|
+
"""Return cached prompt metadata as structured data."""
|
|
142
|
+
|
|
143
|
+
data = self.get_cached_data()
|
|
144
|
+
if not data:
|
|
145
|
+
return DetectedPrompts()
|
|
146
|
+
return data.prompts
|
|
147
|
+
|
|
148
|
+
def get_ignored_headers(self) -> list[str]:
|
|
149
|
+
"""Headers that should be ignored when forwarding CLI values."""
|
|
150
|
+
|
|
151
|
+
return list(self.ignores_header)
|
|
152
|
+
|
|
153
|
+
def get_redacted_headers(self) -> list[str]:
|
|
154
|
+
"""Headers that must always be removed before forwarding."""
|
|
155
|
+
|
|
156
|
+
return list(getattr(self, "REDACTED_HEADERS", []))
|
|
157
|
+
|
|
158
|
+
def get_version(self) -> str:
|
|
159
|
+
"""Get the Codex CLI version.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Version string or "unknown" if not available
|
|
163
|
+
"""
|
|
164
|
+
data = self.get_cached_data()
|
|
165
|
+
return data.codex_version if data else "unknown"
|
|
166
|
+
|
|
167
|
+
def get_cli_path(self) -> list[str] | None:
|
|
168
|
+
"""Get the Codex CLI command with caching.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Command list to execute Codex CLI if found, None otherwise
|
|
172
|
+
"""
|
|
173
|
+
info = self._cli_service.get_cli_info("codex")
|
|
174
|
+
return info["command"] if info["is_available"] else None
|
|
175
|
+
|
|
176
|
+
def get_binary_path(self) -> list[str] | None:
|
|
177
|
+
"""Alias for get_cli_path for backward compatibility."""
|
|
178
|
+
return self.get_cli_path()
|
|
179
|
+
|
|
180
|
+
def get_cli_health_info(self) -> CodexCliInfo:
|
|
181
|
+
"""Get lightweight CLI health info using centralized detection, cached locally.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
CodexCliInfo with availability, version, and binary path
|
|
185
|
+
"""
|
|
186
|
+
from .models import CodexCliInfo, CodexCliStatus
|
|
187
|
+
|
|
188
|
+
if self._cli_info is not None:
|
|
189
|
+
return self._cli_info
|
|
190
|
+
|
|
191
|
+
info = self._cli_service.get_cli_info("codex")
|
|
192
|
+
status = (
|
|
193
|
+
CodexCliStatus.AVAILABLE
|
|
194
|
+
if info["is_available"]
|
|
195
|
+
else CodexCliStatus.NOT_INSTALLED
|
|
196
|
+
)
|
|
197
|
+
cli_info = CodexCliInfo(
|
|
198
|
+
status=status,
|
|
199
|
+
version=info.get("version"),
|
|
200
|
+
binary_path=info.get("path"),
|
|
201
|
+
)
|
|
202
|
+
self._cli_info = cli_info
|
|
203
|
+
return cli_info
|
|
204
|
+
|
|
205
|
+
@async_ttl_cache(maxsize=16, ttl=900.0) # 15 minute cache for version
|
|
206
|
+
async def _get_codex_version(self) -> str:
|
|
207
|
+
"""Get Codex CLI version with caching."""
|
|
208
|
+
try:
|
|
209
|
+
# Custom parser for Codex version format
|
|
210
|
+
def parse_codex_version(output: str) -> str:
|
|
211
|
+
# Handle "codex 0.21.0" format
|
|
212
|
+
if " " in output:
|
|
213
|
+
return output.split()[-1]
|
|
214
|
+
return output
|
|
215
|
+
|
|
216
|
+
# Use centralized CLI detection
|
|
217
|
+
result = await self._cli_service.detect_cli(
|
|
218
|
+
binary_name="codex",
|
|
219
|
+
package_name="@openai/codex",
|
|
220
|
+
version_flag="--version",
|
|
221
|
+
version_parser=parse_codex_version,
|
|
222
|
+
cache_key="codex_version",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if result.is_available and result.version:
|
|
226
|
+
return result.version
|
|
227
|
+
else:
|
|
228
|
+
raise FileNotFoundError("Codex CLI not found")
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.warning(
|
|
232
|
+
"codex_version_detection_failed", error=str(e), category="plugin"
|
|
233
|
+
)
|
|
234
|
+
return "unknown"
|
|
235
|
+
|
|
236
|
+
async def _detect_codex_headers(self, version: str) -> CodexCacheData:
|
|
237
|
+
"""Execute Codex CLI with proxy to capture headers and instructions."""
|
|
238
|
+
# Data captured from the request
|
|
239
|
+
captured_data: dict[str, Any] = {}
|
|
240
|
+
|
|
241
|
+
async def capture_handler(request: Request) -> Response:
|
|
242
|
+
"""Capture the Codex CLI request."""
|
|
243
|
+
# Capture headers and request metadata
|
|
244
|
+
headers_dict = extract_request_headers(request)
|
|
245
|
+
captured_data["headers"] = headers_dict
|
|
246
|
+
captured_data["method"] = request.method
|
|
247
|
+
captured_data["url"] = str(request.url)
|
|
248
|
+
captured_data["path"] = request.url.path
|
|
249
|
+
captured_data["query_params"] = (
|
|
250
|
+
dict(request.query_params) if request.query_params else {}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Capture raw body
|
|
254
|
+
raw_body = await request.body()
|
|
255
|
+
captured_data["body"] = raw_body
|
|
256
|
+
|
|
257
|
+
# Parse body as JSON if possible
|
|
258
|
+
try:
|
|
259
|
+
if raw_body:
|
|
260
|
+
captured_data["body_json"] = json.loads(raw_body.decode("utf-8"))
|
|
261
|
+
else:
|
|
262
|
+
captured_data["body_json"] = None
|
|
263
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
264
|
+
logger.debug("body_parsing_failed", error=str(e), category="plugin")
|
|
265
|
+
captured_data["body_json"] = None
|
|
266
|
+
|
|
267
|
+
logger.debug(
|
|
268
|
+
"request_captured",
|
|
269
|
+
method=request.method,
|
|
270
|
+
path=request.url.path,
|
|
271
|
+
headers_count=len(headers_dict),
|
|
272
|
+
body_size=len(raw_body),
|
|
273
|
+
category="plugin",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Return a mock response to satisfy Codex CLI
|
|
277
|
+
return Response(
|
|
278
|
+
content='{"choices": [{"message": {"content": "Test response"}}]}',
|
|
279
|
+
media_type="application/json",
|
|
280
|
+
status_code=200,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Create temporary FastAPI app
|
|
284
|
+
temp_app = FastAPI()
|
|
285
|
+
# Current Codex endpoint used by CLI
|
|
286
|
+
temp_app.post("/backend-api/codex/responses")(capture_handler)
|
|
287
|
+
|
|
288
|
+
# from starlette.middleware.base import BaseHTTPMiddleware
|
|
289
|
+
# from starlette.requests import Request
|
|
290
|
+
#
|
|
291
|
+
# Another way to recover the headers
|
|
292
|
+
# class DumpHeadersMiddleware(BaseHTTPMiddleware):
|
|
293
|
+
# async def dispatch(self, request: Request, call_next):
|
|
294
|
+
# # Print all headers
|
|
295
|
+
# print("Request Headers:")
|
|
296
|
+
# for name, value in request.headers.items():
|
|
297
|
+
# print(f"{name}: {value}")
|
|
298
|
+
# response = await call_next(request)
|
|
299
|
+
# return response
|
|
300
|
+
#
|
|
301
|
+
# temp_app.add_middleware(DumpHeadersMiddleware)
|
|
302
|
+
|
|
303
|
+
# Find available port
|
|
304
|
+
sock = socket.socket()
|
|
305
|
+
sock.bind(("", 0))
|
|
306
|
+
port = sock.getsockname()[1]
|
|
307
|
+
sock.close()
|
|
308
|
+
|
|
309
|
+
# Start server in background
|
|
310
|
+
from uvicorn import Config, Server
|
|
311
|
+
|
|
312
|
+
config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
|
|
313
|
+
server = Server(config)
|
|
314
|
+
|
|
315
|
+
server_ready = asyncio.Event()
|
|
316
|
+
|
|
317
|
+
@temp_app.on_event("startup")
|
|
318
|
+
async def signal_server_ready() -> None:
|
|
319
|
+
"""Mark the in-process server as ready once startup completes."""
|
|
320
|
+
|
|
321
|
+
server_ready.set()
|
|
322
|
+
|
|
323
|
+
logger.debug("start", category="plugin")
|
|
324
|
+
server_task = asyncio.create_task(server.serve())
|
|
325
|
+
ready_task = asyncio.create_task(server_ready.wait())
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
done, _pending = await asyncio.wait(
|
|
329
|
+
{ready_task, server_task},
|
|
330
|
+
timeout=5,
|
|
331
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
332
|
+
)
|
|
333
|
+
if ready_task in done:
|
|
334
|
+
await ready_task
|
|
335
|
+
elif server_task in done:
|
|
336
|
+
await server_task
|
|
337
|
+
raise RuntimeError(
|
|
338
|
+
"Codex detection server exited before signalling readiness"
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
raise TimeoutError(
|
|
342
|
+
"Timed out waiting for Codex detection server startup"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
stdout, stderr = b"", b""
|
|
346
|
+
|
|
347
|
+
# Determine home directory mode based on configuration
|
|
348
|
+
home_path = os.environ.get("HOME")
|
|
349
|
+
cwd_path = Path.cwd()
|
|
350
|
+
|
|
351
|
+
temp_context: tempfile.TemporaryDirectory[str] | None = None
|
|
352
|
+
if (
|
|
353
|
+
self.codex_settings
|
|
354
|
+
and self.codex_settings.detection_home_mode == "temp"
|
|
355
|
+
):
|
|
356
|
+
temp_context = tempfile.TemporaryDirectory()
|
|
357
|
+
temp_dir_path = Path(temp_context.name)
|
|
358
|
+
home_path = str(temp_dir_path)
|
|
359
|
+
cwd_path = temp_dir_path
|
|
360
|
+
|
|
361
|
+
logger.debug(
|
|
362
|
+
"detection_service_using",
|
|
363
|
+
home_dir=home_path,
|
|
364
|
+
cwd=cwd_path,
|
|
365
|
+
category="plugin",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
# Execute Codex CLI with proxy
|
|
370
|
+
env: dict[str, str] = dict(os.environ)
|
|
371
|
+
env["OPENAI_BASE_URL"] = f"http://127.0.0.1:{port}/backend-api/codex"
|
|
372
|
+
env["OPENAI_API_KEY"] = "dummy-key-for-detection"
|
|
373
|
+
if home_path is not None:
|
|
374
|
+
env["HOME"] = home_path
|
|
375
|
+
del env["OPENAI_API_KEY"]
|
|
376
|
+
|
|
377
|
+
# Get codex command from CLI service
|
|
378
|
+
cli_info = self._cli_service.get_cli_info("codex")
|
|
379
|
+
if not cli_info["is_available"] or not cli_info["command"]:
|
|
380
|
+
raise FileNotFoundError("Codex CLI not found for header detection")
|
|
381
|
+
|
|
382
|
+
# Prepare command
|
|
383
|
+
cmd = cli_info["command"] + [
|
|
384
|
+
"exec",
|
|
385
|
+
"--cd",
|
|
386
|
+
str(cwd_path),
|
|
387
|
+
"--skip-git-repo-check",
|
|
388
|
+
"test",
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
process = await asyncio.create_subprocess_exec(
|
|
392
|
+
*cmd,
|
|
393
|
+
env=env,
|
|
394
|
+
stdout=asyncio.subprocess.PIPE,
|
|
395
|
+
stderr=asyncio.subprocess.PIPE,
|
|
396
|
+
)
|
|
397
|
+
# Wait for process with timeout
|
|
398
|
+
try:
|
|
399
|
+
await asyncio.wait_for(process.wait(), timeout=300)
|
|
400
|
+
except TimeoutError:
|
|
401
|
+
process.kill()
|
|
402
|
+
await process.wait()
|
|
403
|
+
|
|
404
|
+
stdout = await process.stdout.read() if process.stdout else b""
|
|
405
|
+
stderr = await process.stderr.read() if process.stderr else b""
|
|
406
|
+
|
|
407
|
+
finally:
|
|
408
|
+
# Clean up temporary directory if used
|
|
409
|
+
if temp_context is not None:
|
|
410
|
+
temp_context.cleanup()
|
|
411
|
+
|
|
412
|
+
finally:
|
|
413
|
+
if not ready_task.done():
|
|
414
|
+
ready_task.cancel()
|
|
415
|
+
with suppress(asyncio.CancelledError):
|
|
416
|
+
await ready_task
|
|
417
|
+
|
|
418
|
+
server.should_exit = True
|
|
419
|
+
await server_task
|
|
420
|
+
|
|
421
|
+
if not captured_data:
|
|
422
|
+
logger.error(
|
|
423
|
+
"failed_to_capture_codex_cli_request",
|
|
424
|
+
stdout=stdout.decode(errors="ignore"),
|
|
425
|
+
stderr=stderr.decode(errors="ignore"),
|
|
426
|
+
category="plugin",
|
|
427
|
+
)
|
|
428
|
+
raise RuntimeError("Failed to capture Codex CLI request")
|
|
429
|
+
|
|
430
|
+
# Sanitize headers/body for cache
|
|
431
|
+
headers_dict = (
|
|
432
|
+
self._sanitize_headers_for_cache(captured_data.get("headers", {}))
|
|
433
|
+
if self._redact_sensitive_cache
|
|
434
|
+
else captured_data.get("headers", {})
|
|
435
|
+
)
|
|
436
|
+
body_json = (
|
|
437
|
+
self._sanitize_body_json_for_cache(captured_data.get("body_json"))
|
|
438
|
+
if self._redact_sensitive_cache
|
|
439
|
+
else captured_data.get("body_json")
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
prompts = DetectedPrompts.from_body(body_json)
|
|
443
|
+
|
|
444
|
+
return CodexCacheData(
|
|
445
|
+
codex_version=version,
|
|
446
|
+
headers=DetectedHeaders(headers_dict),
|
|
447
|
+
prompts=prompts,
|
|
448
|
+
body_json=body_json,
|
|
449
|
+
method=captured_data.get("method"),
|
|
450
|
+
url=captured_data.get("url"),
|
|
451
|
+
path=captured_data.get("path"),
|
|
452
|
+
query_params=captured_data.get("query_params"),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def _load_from_cache(self, version: str) -> CodexCacheData | None:
|
|
456
|
+
"""Load cached data for specific Codex version."""
|
|
457
|
+
cache_file = self.cache_dir / f"codex_headers_{version}.json"
|
|
458
|
+
|
|
459
|
+
if not cache_file.exists():
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
with cache_file.open("r") as f:
|
|
463
|
+
data = json.load(f)
|
|
464
|
+
return CodexCacheData.model_validate(data)
|
|
465
|
+
|
|
466
|
+
def _save_to_cache(self, data: CodexCacheData) -> None:
|
|
467
|
+
"""Save detection data to cache."""
|
|
468
|
+
cache_file = self.cache_dir / f"codex_headers_{data.codex_version}.json"
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
with cache_file.open("w") as f:
|
|
472
|
+
json.dump(data.model_dump(), f, indent=2, default=str)
|
|
473
|
+
logger.debug(
|
|
474
|
+
"cache_saved",
|
|
475
|
+
file=str(cache_file),
|
|
476
|
+
version=data.codex_version,
|
|
477
|
+
category="plugin",
|
|
478
|
+
)
|
|
479
|
+
except Exception as e:
|
|
480
|
+
logger.warning(
|
|
481
|
+
"cache_save_failed",
|
|
482
|
+
file=str(cache_file),
|
|
483
|
+
error=str(e),
|
|
484
|
+
category="plugin",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def _get_fallback_data(self) -> CodexCacheData:
|
|
488
|
+
"""Get fallback data when detection fails."""
|
|
489
|
+
logger.warning("using_fallback_codex_data", category="plugin")
|
|
490
|
+
|
|
491
|
+
# Load fallback data from package data file
|
|
492
|
+
package_data_file = (
|
|
493
|
+
Path(__file__).resolve().parents[2] / "data" / "codex_headers_fallback.json"
|
|
494
|
+
)
|
|
495
|
+
with package_data_file.open("r") as f:
|
|
496
|
+
fallback_data_dict = json.load(f)
|
|
497
|
+
return CodexCacheData.model_validate(fallback_data_dict)
|
|
498
|
+
|
|
499
|
+
def invalidate_cache(self) -> None:
|
|
500
|
+
"""Clear all cached detection data."""
|
|
501
|
+
# Clear the async cache for _get_codex_version
|
|
502
|
+
if hasattr(self._get_codex_version, "cache_clear"):
|
|
503
|
+
self._get_codex_version.cache_clear()
|
|
504
|
+
self._cli_info = None
|
|
505
|
+
logger.debug("detection_cache_cleared", category="plugin")
|
|
506
|
+
|
|
507
|
+
# --- Helpers ---
|
|
508
|
+
def _sanitize_headers_for_cache(self, headers: dict[str, str]) -> dict[str, str]:
|
|
509
|
+
"""Redact sensitive headers for cache while preserving keys and order."""
|
|
510
|
+
sanitized: dict[str, str] = {}
|
|
511
|
+
for k, v in headers.items():
|
|
512
|
+
lk = k.lower()
|
|
513
|
+
if lk in self.REDACTED_HEADERS:
|
|
514
|
+
sanitized[lk] = "" if len(str(v)) < 8 else str(v)[:8] + "..."
|
|
515
|
+
else:
|
|
516
|
+
sanitized[lk] = v
|
|
517
|
+
return sanitized
|
|
518
|
+
|
|
519
|
+
def _sanitize_body_json_for_cache(
|
|
520
|
+
self, body: dict[str, Any] | None
|
|
521
|
+
) -> dict[str, Any] | None:
|
|
522
|
+
if body is None:
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
def redact(obj: Any) -> Any:
|
|
526
|
+
if isinstance(obj, dict):
|
|
527
|
+
out: dict[str, Any] = {}
|
|
528
|
+
for k, v in obj.items():
|
|
529
|
+
if k == "conversation_id":
|
|
530
|
+
out[k] = ""
|
|
531
|
+
else:
|
|
532
|
+
out[k] = redact(v)
|
|
533
|
+
return out
|
|
534
|
+
elif isinstance(obj, list):
|
|
535
|
+
return [redact(x) for x in obj]
|
|
536
|
+
else:
|
|
537
|
+
return obj
|
|
538
|
+
|
|
539
|
+
return cast(dict[str, Any] | None, redact(body))
|
|
540
|
+
|
|
541
|
+
def get_system_prompt(self, mode: str | None = None) -> dict[str, Any]:
|
|
542
|
+
"""Return an instructions dict for injection based on cached prompts."""
|
|
543
|
+
prompts = self.get_detected_prompts()
|
|
544
|
+
return prompts.instructions_payload()
|