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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Configuration for the Command Replay plugin."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CommandReplayConfig(BaseModel):
|
|
7
|
+
"""Configuration for command replay generation.
|
|
8
|
+
|
|
9
|
+
Generates curl and xh commands for provider requests to enable
|
|
10
|
+
easy replay and debugging of API calls.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Enable/disable entire plugin
|
|
14
|
+
enabled: bool = Field(
|
|
15
|
+
default=True, description="Enable or disable the command replay plugin"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Command generation options
|
|
19
|
+
generate_curl: bool = Field(default=True, description="Generate curl commands")
|
|
20
|
+
generate_xh: bool = Field(default=True, description="Generate xh commands")
|
|
21
|
+
|
|
22
|
+
# Formatting options
|
|
23
|
+
pretty_format: bool = Field(
|
|
24
|
+
default=True,
|
|
25
|
+
description="Use pretty formatting with line continuations for readability",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Request filtering
|
|
29
|
+
include_url_patterns: list[str] = Field(
|
|
30
|
+
default_factory=lambda: [
|
|
31
|
+
"api.anthropic.com",
|
|
32
|
+
"api.openai.com",
|
|
33
|
+
"claude.ai",
|
|
34
|
+
"chatgpt.com",
|
|
35
|
+
],
|
|
36
|
+
description="Only generate commands for URLs matching these patterns",
|
|
37
|
+
)
|
|
38
|
+
exclude_url_patterns: list[str] = Field(
|
|
39
|
+
default_factory=list,
|
|
40
|
+
description="Skip generating commands for URLs matching these patterns",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# File output control
|
|
44
|
+
log_dir: str = Field(
|
|
45
|
+
default="/tmp/ccproxy/command_replay",
|
|
46
|
+
description="Directory for command replay files",
|
|
47
|
+
)
|
|
48
|
+
write_to_files: bool = Field(default=True, description="Write commands to files")
|
|
49
|
+
separate_files_per_command: bool = Field(
|
|
50
|
+
default=True,
|
|
51
|
+
description="Create separate files for curl and xh (False = single combined file)",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Console output control
|
|
55
|
+
log_to_console: bool = Field(
|
|
56
|
+
default=False, description="Log commands to console via logger"
|
|
57
|
+
)
|
|
58
|
+
log_level: str = Field(
|
|
59
|
+
default="TRACE",
|
|
60
|
+
description="Log level for command output (DEBUG, INFO, WARNING)",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Request type filtering
|
|
64
|
+
only_provider_requests: bool = Field(
|
|
65
|
+
default=False,
|
|
66
|
+
description="Only generate commands for provider requests (not client requests)",
|
|
67
|
+
)
|
|
68
|
+
include_client_requests: bool = Field(
|
|
69
|
+
default=True,
|
|
70
|
+
description="Generate commands for client requests to non-provider URLs",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict()
|
|
74
|
+
|
|
75
|
+
def should_generate_for_url(
|
|
76
|
+
self, url: str, is_provider_request: bool | None = None
|
|
77
|
+
) -> bool:
|
|
78
|
+
"""Check if commands should be generated for the given URL.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
url: The request URL to check
|
|
82
|
+
is_provider_request: Whether this is a provider request (None = auto-detect)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if commands should be generated for this URL
|
|
86
|
+
"""
|
|
87
|
+
# Check exclude patterns first
|
|
88
|
+
if self.exclude_url_patterns:
|
|
89
|
+
if any(pattern in url for pattern in self.exclude_url_patterns):
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# Auto-detect if this is a provider request if not specified
|
|
93
|
+
if is_provider_request is None:
|
|
94
|
+
provider_domains = [
|
|
95
|
+
"api.anthropic.com",
|
|
96
|
+
"claude.ai",
|
|
97
|
+
"api.openai.com",
|
|
98
|
+
"chatgpt.com",
|
|
99
|
+
]
|
|
100
|
+
is_provider_request = any(
|
|
101
|
+
domain in url.lower() for domain in provider_domains
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Apply request type filtering
|
|
105
|
+
if self.only_provider_requests and not is_provider_request:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
if not self.include_client_requests and not is_provider_request:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
# For provider requests, check include patterns
|
|
112
|
+
if is_provider_request:
|
|
113
|
+
if self.include_url_patterns:
|
|
114
|
+
return any(pattern in url for pattern in self.include_url_patterns)
|
|
115
|
+
else:
|
|
116
|
+
# For client requests, be more permissive
|
|
117
|
+
# Only filter if there are specific include patterns that don't match
|
|
118
|
+
if self.include_url_patterns:
|
|
119
|
+
# If include patterns are all provider domains, allow client requests
|
|
120
|
+
provider_only = all(
|
|
121
|
+
any(
|
|
122
|
+
provider in pattern.lower()
|
|
123
|
+
for provider in ["anthropic", "openai", "claude", "chatgpt"]
|
|
124
|
+
)
|
|
125
|
+
for pattern in self.include_url_patterns
|
|
126
|
+
)
|
|
127
|
+
if provider_only:
|
|
128
|
+
return True
|
|
129
|
+
# Otherwise apply normal include pattern matching
|
|
130
|
+
return any(pattern in url for pattern in self.include_url_patterns)
|
|
131
|
+
|
|
132
|
+
# Default: generate for all URLs if no patterns specified
|
|
133
|
+
return True
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""File formatter for command replay output."""
|
|
2
|
+
|
|
3
|
+
import stat
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import aiofiles
|
|
11
|
+
|
|
12
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
13
|
+
from ccproxy.utils.command_line import generate_curl_shell_script
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = get_plugin_logger()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CommandFileFormatter:
|
|
20
|
+
"""Formats and writes command replay data to files."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
log_dir: str = "/tmp/ccproxy/command_replay",
|
|
25
|
+
enabled: bool = True,
|
|
26
|
+
separate_files_per_command: bool = False,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize with configuration.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
log_dir: Directory for command replay files
|
|
32
|
+
enabled: Enable file writing
|
|
33
|
+
separate_files_per_command: Create separate files for curl/xh vs combined
|
|
34
|
+
"""
|
|
35
|
+
self.enabled = enabled
|
|
36
|
+
self.log_dir = Path(log_dir)
|
|
37
|
+
self.separate_files_per_command = separate_files_per_command
|
|
38
|
+
|
|
39
|
+
if self.enabled:
|
|
40
|
+
# Create log directory if it doesn't exist
|
|
41
|
+
try:
|
|
42
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
except OSError as e:
|
|
44
|
+
logger.error(
|
|
45
|
+
"failed_to_create_command_replay_directory",
|
|
46
|
+
log_dir=str(self.log_dir),
|
|
47
|
+
error=str(e),
|
|
48
|
+
exc_info=e,
|
|
49
|
+
)
|
|
50
|
+
# Disable file writing if we can't create the directory
|
|
51
|
+
self.enabled = False
|
|
52
|
+
|
|
53
|
+
# Track which files we've already created (for logging purposes only)
|
|
54
|
+
self._created_files: set[str] = set()
|
|
55
|
+
|
|
56
|
+
def _compose_file_id(self, request_id: str | None) -> str:
|
|
57
|
+
"""Generate base file ID from request ID.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
request_id: Request ID for correlation
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Base file ID string
|
|
64
|
+
"""
|
|
65
|
+
if request_id:
|
|
66
|
+
# Clean up request ID for filesystem safety
|
|
67
|
+
safe_id = "".join(
|
|
68
|
+
c if c.isalnum() or c in "-_" else "_" for c in request_id
|
|
69
|
+
)
|
|
70
|
+
return safe_id[:50] # Limit length
|
|
71
|
+
else:
|
|
72
|
+
return str(uuid.uuid4())[:8]
|
|
73
|
+
|
|
74
|
+
def _compose_file_id_with_timestamp(self, request_id: str | None) -> str:
|
|
75
|
+
"""Build filename ID with timestamp suffix for better organization.
|
|
76
|
+
|
|
77
|
+
Format: {base_id}_{timestamp}_{nanos}
|
|
78
|
+
Where timestamp is in format: YYYYMMDD_HHMMSS_microseconds
|
|
79
|
+
And nanos is a counter to prevent collisions
|
|
80
|
+
"""
|
|
81
|
+
base_id = self._compose_file_id(request_id)
|
|
82
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
83
|
+
|
|
84
|
+
# Add a high-resolution timestamp with nanoseconds for uniqueness
|
|
85
|
+
nanos = time.time_ns() % 1000000 # Get nanosecond portion
|
|
86
|
+
return f"{base_id}_{timestamp}_{nanos:06d}"
|
|
87
|
+
|
|
88
|
+
def should_write_files(self) -> bool:
|
|
89
|
+
"""Check if file writing is enabled."""
|
|
90
|
+
return bool(self.enabled)
|
|
91
|
+
|
|
92
|
+
async def write_commands(
|
|
93
|
+
self,
|
|
94
|
+
request_id: str,
|
|
95
|
+
curl_command: str,
|
|
96
|
+
xh_command: str,
|
|
97
|
+
provider: str | None = None,
|
|
98
|
+
timestamp_prefix: str | None = None,
|
|
99
|
+
method: str | None = None,
|
|
100
|
+
url: str | None = None,
|
|
101
|
+
headers: dict[str, str] | None = None,
|
|
102
|
+
body: Any = None,
|
|
103
|
+
is_json: bool = False,
|
|
104
|
+
) -> list[str]:
|
|
105
|
+
"""Write command replay data to files.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
request_id: Request ID for correlation
|
|
109
|
+
curl_command: Generated curl command
|
|
110
|
+
xh_command: Generated xh command
|
|
111
|
+
provider: Provider name (anthropic, openai, etc.)
|
|
112
|
+
timestamp_prefix: Optional timestamp prefix from RequestContext
|
|
113
|
+
method: HTTP method for shell script generation
|
|
114
|
+
url: Request URL for shell script generation
|
|
115
|
+
headers: HTTP headers for shell script generation
|
|
116
|
+
body: Request body for shell script generation
|
|
117
|
+
is_json: Whether body is JSON for shell script generation
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of file paths that were written
|
|
121
|
+
"""
|
|
122
|
+
if not self.enabled:
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
written_files = []
|
|
126
|
+
|
|
127
|
+
# Use provided timestamp prefix or generate our own
|
|
128
|
+
if timestamp_prefix:
|
|
129
|
+
base_id = f"{self._compose_file_id(request_id)}_{timestamp_prefix}"
|
|
130
|
+
else:
|
|
131
|
+
base_id = self._compose_file_id_with_timestamp(request_id)
|
|
132
|
+
|
|
133
|
+
# Add provider to filename if available
|
|
134
|
+
if provider:
|
|
135
|
+
base_id = f"{base_id}_{provider}"
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
if self.separate_files_per_command:
|
|
139
|
+
# Write separate files for curl and xh
|
|
140
|
+
if curl_command:
|
|
141
|
+
curl_file = await self._write_single_command_file(
|
|
142
|
+
base_id, "curl", curl_command, request_id
|
|
143
|
+
)
|
|
144
|
+
if curl_file:
|
|
145
|
+
written_files.append(curl_file)
|
|
146
|
+
|
|
147
|
+
if xh_command:
|
|
148
|
+
xh_file = await self._write_single_command_file(
|
|
149
|
+
base_id, "xh", xh_command, request_id
|
|
150
|
+
)
|
|
151
|
+
if xh_file:
|
|
152
|
+
written_files.append(xh_file)
|
|
153
|
+
else:
|
|
154
|
+
# Write combined file with both commands
|
|
155
|
+
combined_file = await self._write_combined_command_file(
|
|
156
|
+
base_id, curl_command, xh_command, request_id, provider
|
|
157
|
+
)
|
|
158
|
+
if combined_file:
|
|
159
|
+
written_files.append(combined_file)
|
|
160
|
+
|
|
161
|
+
# Generate executable shell script if we have raw request data
|
|
162
|
+
if method and url:
|
|
163
|
+
shell_script_file = await self._write_shell_script_file(
|
|
164
|
+
base_id, request_id, method, url, headers, body, is_json, provider
|
|
165
|
+
)
|
|
166
|
+
if shell_script_file:
|
|
167
|
+
written_files.append(shell_script_file)
|
|
168
|
+
|
|
169
|
+
# Make files executable
|
|
170
|
+
await self._make_files_executable(written_files)
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(
|
|
174
|
+
"command_replay_file_write_error",
|
|
175
|
+
request_id=request_id,
|
|
176
|
+
error=str(e),
|
|
177
|
+
exc_info=e,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return written_files
|
|
181
|
+
|
|
182
|
+
async def _write_single_command_file(
|
|
183
|
+
self,
|
|
184
|
+
base_id: str,
|
|
185
|
+
command_type: str,
|
|
186
|
+
command: str,
|
|
187
|
+
request_id: str,
|
|
188
|
+
) -> str | None:
|
|
189
|
+
"""Write a single command to its own file.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
base_id: Base filename identifier
|
|
193
|
+
command_type: Command type (curl, xh)
|
|
194
|
+
command: Command string to write
|
|
195
|
+
request_id: Request ID for logging
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
File path if successful, None if failed
|
|
199
|
+
"""
|
|
200
|
+
file_path = self.log_dir / f"{base_id}_{command_type}.sh"
|
|
201
|
+
|
|
202
|
+
# Log file creation (only once per unique file path)
|
|
203
|
+
if str(file_path) not in self._created_files:
|
|
204
|
+
self._created_files.add(str(file_path))
|
|
205
|
+
logger.debug(
|
|
206
|
+
"command_replay_file_created",
|
|
207
|
+
request_id=request_id,
|
|
208
|
+
command_type=command_type,
|
|
209
|
+
file_path=str(file_path),
|
|
210
|
+
mode="separate",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
|
215
|
+
await f.write("#!/usr/bin/env bash\n")
|
|
216
|
+
await f.write(f"# {command_type.upper()} Command Replay\n")
|
|
217
|
+
await f.write(f"# Request ID: {request_id}\n")
|
|
218
|
+
await f.write(f"# Generated: {datetime.now().isoformat()}\n")
|
|
219
|
+
await f.write("#\n")
|
|
220
|
+
await f.write(
|
|
221
|
+
f"# Run this file directly: ./{base_id}_{command_type}.sh\n"
|
|
222
|
+
)
|
|
223
|
+
await f.write("\n")
|
|
224
|
+
await f.write(command)
|
|
225
|
+
await f.write("\n")
|
|
226
|
+
|
|
227
|
+
return str(file_path)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error(
|
|
231
|
+
"command_replay_single_file_write_error",
|
|
232
|
+
request_id=request_id,
|
|
233
|
+
command_type=command_type,
|
|
234
|
+
file_path=str(file_path),
|
|
235
|
+
error=str(e),
|
|
236
|
+
)
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
async def _write_combined_command_file(
|
|
240
|
+
self,
|
|
241
|
+
base_id: str,
|
|
242
|
+
curl_command: str,
|
|
243
|
+
xh_command: str,
|
|
244
|
+
request_id: str,
|
|
245
|
+
provider: str | None = None,
|
|
246
|
+
) -> str | None:
|
|
247
|
+
"""Write both commands to a single combined file.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
base_id: Base filename identifier
|
|
251
|
+
curl_command: curl command string
|
|
252
|
+
xh_command: xh command string
|
|
253
|
+
request_id: Request ID for logging
|
|
254
|
+
provider: Provider name for header
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
File path if successful, None if failed
|
|
258
|
+
"""
|
|
259
|
+
file_path = self.log_dir / f"{base_id}_commands.sh"
|
|
260
|
+
|
|
261
|
+
# Log file creation (only once per unique file path)
|
|
262
|
+
if str(file_path) not in self._created_files:
|
|
263
|
+
self._created_files.add(str(file_path))
|
|
264
|
+
logger.debug(
|
|
265
|
+
"command_replay_file_created",
|
|
266
|
+
request_id=request_id,
|
|
267
|
+
command_type="combined",
|
|
268
|
+
file_path=str(file_path),
|
|
269
|
+
mode="combined",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
|
274
|
+
# Write shebang and header
|
|
275
|
+
await f.write("#!/usr/bin/env bash\n")
|
|
276
|
+
await f.write("# Command Replay File\n")
|
|
277
|
+
await f.write(f"# Request ID: {request_id}\n")
|
|
278
|
+
if provider:
|
|
279
|
+
await f.write(f"# Provider: {provider}\n")
|
|
280
|
+
await f.write(f"# Generated: {datetime.now().isoformat()}\n")
|
|
281
|
+
await f.write("#\n")
|
|
282
|
+
await f.write("# This file contains both curl and xh commands.\n")
|
|
283
|
+
await f.write("# Uncomment the command you want to run.\n")
|
|
284
|
+
await f.write("\n")
|
|
285
|
+
|
|
286
|
+
# Write curl command
|
|
287
|
+
if curl_command:
|
|
288
|
+
await f.write("# CURL Command\n")
|
|
289
|
+
await f.write("# " + "=" * 50 + "\n")
|
|
290
|
+
# Comment out the command so it doesn't run accidentally
|
|
291
|
+
for line in curl_command.split("\n"):
|
|
292
|
+
if line.strip():
|
|
293
|
+
await f.write(f"# {line}\n")
|
|
294
|
+
else:
|
|
295
|
+
await f.write("#\n")
|
|
296
|
+
await f.write("\n")
|
|
297
|
+
|
|
298
|
+
# Write xh command
|
|
299
|
+
if xh_command:
|
|
300
|
+
await f.write("# XH Command\n")
|
|
301
|
+
await f.write("# " + "=" * 50 + "\n")
|
|
302
|
+
# Comment out the command so it doesn't run accidentally
|
|
303
|
+
for line in xh_command.split("\n"):
|
|
304
|
+
if line.strip():
|
|
305
|
+
await f.write(f"# {line}\n")
|
|
306
|
+
else:
|
|
307
|
+
await f.write("#\n")
|
|
308
|
+
await f.write("\n")
|
|
309
|
+
|
|
310
|
+
# Add footer with instructions
|
|
311
|
+
await f.write("# " + "=" * 60 + "\n")
|
|
312
|
+
await f.write("# Instructions:\n")
|
|
313
|
+
await f.write("# 1. Uncomment the command you want to use\n")
|
|
314
|
+
await f.write("# 2. Make sure you have curl or xh installed\n")
|
|
315
|
+
await f.write("# 3. Run: chmod +x this_file.sh && ./this_file.sh\n")
|
|
316
|
+
await f.write("# " + "=" * 60 + "\n")
|
|
317
|
+
|
|
318
|
+
return str(file_path)
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
logger.error(
|
|
322
|
+
"command_replay_combined_file_write_error",
|
|
323
|
+
request_id=request_id,
|
|
324
|
+
file_path=str(file_path),
|
|
325
|
+
error=str(e),
|
|
326
|
+
)
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
def get_log_dir(self) -> str:
|
|
330
|
+
"""Get the log directory path."""
|
|
331
|
+
return str(self.log_dir)
|
|
332
|
+
|
|
333
|
+
async def _make_files_executable(self, file_paths: list[str]) -> None:
|
|
334
|
+
"""Make the generated files executable.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
file_paths: List of file paths to make executable
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
for file_path_str in file_paths:
|
|
341
|
+
try:
|
|
342
|
+
file_path = Path(file_path_str)
|
|
343
|
+
# Add execute permission for owner, group, and others
|
|
344
|
+
current_mode = file_path.stat().st_mode
|
|
345
|
+
new_mode = current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
346
|
+
file_path.chmod(new_mode)
|
|
347
|
+
|
|
348
|
+
logger.debug(
|
|
349
|
+
"command_replay_file_made_executable",
|
|
350
|
+
file_path=file_path_str,
|
|
351
|
+
)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.warning(
|
|
354
|
+
"command_replay_chmod_failed",
|
|
355
|
+
file_path=file_path_str,
|
|
356
|
+
error=str(e),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
async def _write_shell_script_file(
|
|
360
|
+
self,
|
|
361
|
+
base_id: str,
|
|
362
|
+
request_id: str,
|
|
363
|
+
method: str,
|
|
364
|
+
url: str,
|
|
365
|
+
headers: dict[str, str] | None,
|
|
366
|
+
body: Any,
|
|
367
|
+
is_json: bool,
|
|
368
|
+
provider: str | None = None,
|
|
369
|
+
) -> str | None:
|
|
370
|
+
"""Write an executable shell script file.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
base_id: Base filename identifier
|
|
374
|
+
request_id: Request ID for logging
|
|
375
|
+
method: HTTP method
|
|
376
|
+
url: Request URL
|
|
377
|
+
headers: HTTP headers
|
|
378
|
+
body: Request body
|
|
379
|
+
is_json: Whether body is JSON
|
|
380
|
+
provider: Provider name
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
File path if successful, None if failed
|
|
384
|
+
"""
|
|
385
|
+
file_path = self.log_dir / f"{base_id}_script.sh"
|
|
386
|
+
|
|
387
|
+
# Log file creation
|
|
388
|
+
if str(file_path) not in self._created_files:
|
|
389
|
+
self._created_files.add(str(file_path))
|
|
390
|
+
logger.debug(
|
|
391
|
+
"command_replay_file_created",
|
|
392
|
+
request_id=request_id,
|
|
393
|
+
command_type="shell_script",
|
|
394
|
+
file_path=str(file_path),
|
|
395
|
+
mode="executable",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
# Generate shell-safe script content
|
|
400
|
+
script_content = generate_curl_shell_script(
|
|
401
|
+
method=method,
|
|
402
|
+
url=url,
|
|
403
|
+
headers=headers,
|
|
404
|
+
body=body,
|
|
405
|
+
is_json=is_json,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
|
409
|
+
await f.write("#!/bin/bash\n")
|
|
410
|
+
await f.write("# Executable Shell Script for Request Replay\n")
|
|
411
|
+
await f.write(f"# Request ID: {request_id}\n")
|
|
412
|
+
if provider:
|
|
413
|
+
await f.write(f"# Provider: {provider}\n")
|
|
414
|
+
await f.write(f"# Generated: {datetime.now().isoformat()}\n")
|
|
415
|
+
await f.write(f"# Usage: bash {file_path.name} or ./{file_path.name}\n")
|
|
416
|
+
await f.write("\n")
|
|
417
|
+
await f.write(script_content)
|
|
418
|
+
|
|
419
|
+
return str(file_path)
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.error(
|
|
423
|
+
"command_replay_shell_script_write_error",
|
|
424
|
+
request_id=request_id,
|
|
425
|
+
file_path=str(file_path),
|
|
426
|
+
error=str(e),
|
|
427
|
+
)
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
def cleanup(self) -> None:
|
|
431
|
+
"""Clean up resources (if any)."""
|
|
432
|
+
self._created_files.clear()
|