ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/utils/caching.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Caching utilities for CCProxy.
|
|
2
|
+
|
|
3
|
+
This module provides caching decorators and utilities to improve performance
|
|
4
|
+
by caching frequently accessed data like detection results and auth status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable, Hashable
|
|
11
|
+
from typing import Any, TypeVar
|
|
12
|
+
|
|
13
|
+
from ccproxy.core.logging import TraceBoundLogger, get_logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger: TraceBoundLogger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _trace(message: str, **kwargs: Any) -> None:
|
|
20
|
+
"""Trace-level logger helper with debug fallback."""
|
|
21
|
+
if hasattr(logger, "trace"):
|
|
22
|
+
logger.trace(message, **kwargs)
|
|
23
|
+
else:
|
|
24
|
+
logger.debug(message, **kwargs)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TTLCache:
|
|
31
|
+
"""Thread-safe TTL (Time To Live) cache with LRU eviction."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, maxsize: int = 128, ttl: float = 300.0):
|
|
34
|
+
"""Initialize TTL cache.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
maxsize: Maximum number of entries to cache
|
|
38
|
+
ttl: Time to live for entries in seconds
|
|
39
|
+
"""
|
|
40
|
+
self.maxsize = maxsize
|
|
41
|
+
self.ttl = ttl
|
|
42
|
+
self._cache: dict[Hashable, tuple[Any, float]] = {}
|
|
43
|
+
self._access_order: dict[Hashable, int] = {}
|
|
44
|
+
self._access_counter = 0
|
|
45
|
+
self._lock = threading.RLock()
|
|
46
|
+
|
|
47
|
+
def get(self, key: Hashable) -> Any | None:
|
|
48
|
+
"""Get value from cache."""
|
|
49
|
+
with self._lock:
|
|
50
|
+
if key not in self._cache:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
value, expiry_time = self._cache[key]
|
|
54
|
+
|
|
55
|
+
# Check if expired
|
|
56
|
+
if time.time() > expiry_time:
|
|
57
|
+
self._cache.pop(key, None)
|
|
58
|
+
self._access_order.pop(key, None)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Update access order
|
|
62
|
+
self._access_counter += 1
|
|
63
|
+
self._access_order[key] = self._access_counter
|
|
64
|
+
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
def set(self, key: Hashable, value: Any) -> None:
|
|
68
|
+
"""Set value in cache."""
|
|
69
|
+
with self._lock:
|
|
70
|
+
now = time.time()
|
|
71
|
+
expiry_time = now + self.ttl
|
|
72
|
+
|
|
73
|
+
# Add/update entry
|
|
74
|
+
self._cache[key] = (value, expiry_time)
|
|
75
|
+
self._access_counter += 1
|
|
76
|
+
self._access_order[key] = self._access_counter
|
|
77
|
+
|
|
78
|
+
# Evict expired entries first
|
|
79
|
+
self._evict_expired()
|
|
80
|
+
|
|
81
|
+
# Evict oldest entries if over maxsize
|
|
82
|
+
while len(self._cache) > self.maxsize:
|
|
83
|
+
self._evict_oldest()
|
|
84
|
+
|
|
85
|
+
def delete(self, key: Hashable) -> bool:
|
|
86
|
+
"""Delete entry from cache."""
|
|
87
|
+
with self._lock:
|
|
88
|
+
if key in self._cache:
|
|
89
|
+
del self._cache[key]
|
|
90
|
+
self._access_order.pop(key, None)
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def clear(self) -> None:
|
|
95
|
+
"""Clear all cache entries."""
|
|
96
|
+
with self._lock:
|
|
97
|
+
self._cache.clear()
|
|
98
|
+
self._access_order.clear()
|
|
99
|
+
self._access_counter = 0
|
|
100
|
+
|
|
101
|
+
def _evict_expired(self) -> None:
|
|
102
|
+
"""Remove expired entries."""
|
|
103
|
+
now = time.time()
|
|
104
|
+
expired_keys = [
|
|
105
|
+
key for key, (_, expiry_time) in self._cache.items() if now > expiry_time
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
for key in expired_keys:
|
|
109
|
+
self._cache.pop(key, None)
|
|
110
|
+
self._access_order.pop(key, None)
|
|
111
|
+
|
|
112
|
+
def _evict_oldest(self) -> None:
|
|
113
|
+
"""Remove oldest accessed entry."""
|
|
114
|
+
if not self._access_order:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
oldest_key = min(self._access_order, key=lambda k: self._access_order[k])
|
|
118
|
+
self._cache.pop(oldest_key, None)
|
|
119
|
+
self._access_order.pop(oldest_key, None)
|
|
120
|
+
|
|
121
|
+
def stats(self) -> dict[str, Any]:
|
|
122
|
+
"""Get cache statistics."""
|
|
123
|
+
with self._lock:
|
|
124
|
+
return {
|
|
125
|
+
"size": len(self._cache),
|
|
126
|
+
"maxsize": self.maxsize,
|
|
127
|
+
"ttl": self.ttl,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def ttl_cache(maxsize: int = 128, ttl: float = 300.0) -> Callable[[F], F]:
|
|
132
|
+
"""TTL cache decorator for functions.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
maxsize: Maximum number of entries to cache
|
|
136
|
+
ttl: Time to live for cached results in seconds
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def decorator(func: F) -> F:
|
|
140
|
+
cache = TTLCache(maxsize=maxsize, ttl=ttl)
|
|
141
|
+
|
|
142
|
+
@functools.wraps(func)
|
|
143
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
144
|
+
# Create cache key from function args/kwargs
|
|
145
|
+
key = _make_cache_key(func.__name__, args, kwargs)
|
|
146
|
+
|
|
147
|
+
# Try to get from cache first
|
|
148
|
+
cached_result = cache.get(key)
|
|
149
|
+
if cached_result is not None:
|
|
150
|
+
_trace(
|
|
151
|
+
"cache_hit",
|
|
152
|
+
function=func.__name__,
|
|
153
|
+
key_hash=hash(key) if isinstance(key, tuple) else key,
|
|
154
|
+
)
|
|
155
|
+
return cached_result
|
|
156
|
+
|
|
157
|
+
# Call function and cache result
|
|
158
|
+
result = func(*args, **kwargs)
|
|
159
|
+
cache.set(key, result)
|
|
160
|
+
|
|
161
|
+
_trace(
|
|
162
|
+
"cache_miss_and_set",
|
|
163
|
+
function=func.__name__,
|
|
164
|
+
key_hash=hash(key) if isinstance(key, tuple) else key,
|
|
165
|
+
cache_size=len(cache._cache),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
# Add cache management methods
|
|
171
|
+
wrapper.cache_info = cache.stats # type: ignore
|
|
172
|
+
wrapper.cache_clear = cache.clear # type: ignore
|
|
173
|
+
|
|
174
|
+
return wrapper # type: ignore
|
|
175
|
+
|
|
176
|
+
return decorator
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def async_ttl_cache(
|
|
180
|
+
maxsize: int = 128, ttl: float = 300.0
|
|
181
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
182
|
+
"""TTL cache decorator for async functions.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
maxsize: Maximum number of entries to cache
|
|
186
|
+
ttl: Time to live for cached results in seconds
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
190
|
+
cache = TTLCache(maxsize=maxsize, ttl=ttl)
|
|
191
|
+
|
|
192
|
+
@functools.wraps(func)
|
|
193
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
194
|
+
# Create cache key from function args/kwargs
|
|
195
|
+
key = _make_cache_key(func.__name__, args, kwargs)
|
|
196
|
+
|
|
197
|
+
# Try to get from cache first
|
|
198
|
+
cached_result = cache.get(key)
|
|
199
|
+
if cached_result is not None:
|
|
200
|
+
_trace(
|
|
201
|
+
"async_cache_hit",
|
|
202
|
+
function=func.__name__,
|
|
203
|
+
key_hash=hash(key) if isinstance(key, tuple) else key,
|
|
204
|
+
)
|
|
205
|
+
return cached_result
|
|
206
|
+
|
|
207
|
+
# Call async function and cache result
|
|
208
|
+
result = await func(*args, **kwargs)
|
|
209
|
+
cache.set(key, result)
|
|
210
|
+
|
|
211
|
+
_trace(
|
|
212
|
+
"async_cache_miss_and_set",
|
|
213
|
+
function=func.__name__,
|
|
214
|
+
key_hash=hash(key) if isinstance(key, tuple) else key,
|
|
215
|
+
cache_size=len(cache._cache),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
# Add cache management methods
|
|
221
|
+
wrapper.cache_info = cache.stats # type: ignore
|
|
222
|
+
wrapper.cache_clear = cache.clear # type: ignore
|
|
223
|
+
|
|
224
|
+
return wrapper
|
|
225
|
+
|
|
226
|
+
return decorator
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _make_cache_key(
|
|
230
|
+
func_name: str, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
231
|
+
) -> Hashable:
|
|
232
|
+
"""Create a hashable cache key from function arguments."""
|
|
233
|
+
try:
|
|
234
|
+
# Try to create a simple key for basic types
|
|
235
|
+
key_parts = [func_name]
|
|
236
|
+
|
|
237
|
+
# Add positional args
|
|
238
|
+
for arg in args:
|
|
239
|
+
if hasattr(arg, "__dict__"):
|
|
240
|
+
# For objects, use class name and id (weak ref to avoid memory leaks)
|
|
241
|
+
key_parts.append(f"{type(arg).__name__}:{id(arg)}")
|
|
242
|
+
else:
|
|
243
|
+
key_parts.append(arg)
|
|
244
|
+
|
|
245
|
+
# Add keyword args (sorted for consistency)
|
|
246
|
+
for k, v in sorted(kwargs.items()):
|
|
247
|
+
if hasattr(v, "__dict__"):
|
|
248
|
+
key_parts.append(f"{k}={type(v).__name__}:{id(v)}")
|
|
249
|
+
else:
|
|
250
|
+
key_parts.append(f"{k}={v}")
|
|
251
|
+
|
|
252
|
+
return tuple(key_parts)
|
|
253
|
+
|
|
254
|
+
except (TypeError, ValueError):
|
|
255
|
+
# Fallback to string representation
|
|
256
|
+
return f"{func_name}:{hash(str(args))}:{hash(str(sorted(kwargs.items())))}"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class AuthStatusCache:
|
|
260
|
+
"""Specialized cache for auth status checks with shorter TTL."""
|
|
261
|
+
|
|
262
|
+
def __init__(self, ttl: float = 60.0): # 1 minute TTL for auth status
|
|
263
|
+
"""Initialize auth status cache.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
ttl: Time to live for auth status in seconds
|
|
267
|
+
"""
|
|
268
|
+
self._cache = TTLCache(maxsize=32, ttl=ttl)
|
|
269
|
+
|
|
270
|
+
def get_auth_status(self, provider: str) -> bool | None:
|
|
271
|
+
"""Get cached auth status for provider."""
|
|
272
|
+
return self._cache.get(f"auth_status:{provider}")
|
|
273
|
+
|
|
274
|
+
def set_auth_status(self, provider: str, is_authenticated: bool) -> None:
|
|
275
|
+
"""Cache auth status for provider."""
|
|
276
|
+
self._cache.set(f"auth_status:{provider}", is_authenticated)
|
|
277
|
+
|
|
278
|
+
def invalidate_auth_status(self, provider: str) -> None:
|
|
279
|
+
"""Invalidate auth status for provider."""
|
|
280
|
+
self._cache.delete(f"auth_status:{provider}")
|
|
281
|
+
|
|
282
|
+
def clear(self) -> None:
|
|
283
|
+
"""Clear all auth status cache."""
|
|
284
|
+
self._cache.clear()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# Global instances for common use cases
|
|
288
|
+
_detection_cache = TTLCache(maxsize=64, ttl=600.0) # 10 minute TTL for detection
|
|
289
|
+
_auth_cache = AuthStatusCache(ttl=60.0) # 1 minute TTL for auth status
|
|
290
|
+
_config_cache = TTLCache(maxsize=32, ttl=300.0) # 5 minute TTL for plugin configs
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def cache_detection_result(key: str, result: Any) -> None:
|
|
294
|
+
"""Cache a detection result."""
|
|
295
|
+
_detection_cache.set(f"detection:{key}", result)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_cached_detection_result(key: str) -> Any | None:
|
|
299
|
+
"""Get cached detection result."""
|
|
300
|
+
return _detection_cache.get(f"detection:{key}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def cache_plugin_config(plugin_name: str, config: Any) -> None:
|
|
304
|
+
"""Cache plugin configuration."""
|
|
305
|
+
_config_cache.set(f"plugin_config:{plugin_name}", config)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def get_cached_plugin_config(plugin_name: str) -> Any | None:
|
|
309
|
+
"""Get cached plugin configuration."""
|
|
310
|
+
return _config_cache.get(f"plugin_config:{plugin_name}")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def clear_all_caches() -> None:
|
|
314
|
+
"""Clear all global caches."""
|
|
315
|
+
_detection_cache.clear()
|
|
316
|
+
_auth_cache.clear()
|
|
317
|
+
_config_cache.clear()
|
|
318
|
+
logger.info("all_caches_cleared", category="cache")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def get_cache_stats() -> dict[str, Any]:
|
|
322
|
+
"""Get statistics for all caches."""
|
|
323
|
+
return {
|
|
324
|
+
"detection_cache": _detection_cache.stats(),
|
|
325
|
+
"auth_cache": _auth_cache._cache.stats(),
|
|
326
|
+
"config_cache": _config_cache.stats(),
|
|
327
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Dynamic CLI logging utilities."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
|
|
7
|
+
from .binary_resolver import CLIInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = structlog.get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def log_cli_info(cli_info_dict: dict[str, CLIInfo], context: str = "plugin") -> None:
|
|
14
|
+
"""Log CLI information dynamically for each CLI found.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
cli_info_dict: Dictionary of CLI name -> CLIInfo
|
|
18
|
+
context: Context for logging (e.g., "plugin", "startup", "detection")
|
|
19
|
+
"""
|
|
20
|
+
for cli_name, cli_info in cli_info_dict.items():
|
|
21
|
+
if cli_info["is_available"]:
|
|
22
|
+
logger.debug(
|
|
23
|
+
f"{context}_cli_available",
|
|
24
|
+
cli_name=cli_name,
|
|
25
|
+
version=cli_info["version"],
|
|
26
|
+
source=cli_info["source"],
|
|
27
|
+
path=cli_info["path"],
|
|
28
|
+
command=cli_info["command"],
|
|
29
|
+
package_manager=cli_info["package_manager"],
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
logger.warning(
|
|
33
|
+
f"{context}_cli_unavailable",
|
|
34
|
+
cli_name=cli_name,
|
|
35
|
+
expected_version=cli_info["version"],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def log_plugin_summary(summary: dict[str, Any], plugin_name: str) -> None:
|
|
40
|
+
"""Log plugin summary with dynamic CLI information.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
summary: Plugin summary dictionary
|
|
44
|
+
plugin_name: Name of the plugin
|
|
45
|
+
"""
|
|
46
|
+
# Log basic plugin info
|
|
47
|
+
basic_info = {k: v for k, v in summary.items() if k != "cli_info"}
|
|
48
|
+
logger.debug(
|
|
49
|
+
"plugin_summary",
|
|
50
|
+
plugin_name=plugin_name,
|
|
51
|
+
**basic_info,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Log CLI info dynamically if present
|
|
55
|
+
if "cli_info" in summary:
|
|
56
|
+
log_cli_info(summary["cli_info"], f"{plugin_name}_plugin")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def format_cli_info_for_display(cli_info: CLIInfo) -> dict[str, str]:
|
|
60
|
+
"""Format CLI info for human-readable display.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
cli_info: CLI information dictionary
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Formatted dictionary for display
|
|
67
|
+
"""
|
|
68
|
+
if not cli_info["is_available"]:
|
|
69
|
+
return {
|
|
70
|
+
"status": "unavailable",
|
|
71
|
+
"name": cli_info["name"],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
display_info = {
|
|
75
|
+
"status": "available",
|
|
76
|
+
"name": cli_info["name"],
|
|
77
|
+
"version": cli_info["version"] or "unknown",
|
|
78
|
+
"source": cli_info["source"],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if cli_info["source"] == "path":
|
|
82
|
+
display_info["path"] = cli_info["path"] or "unknown"
|
|
83
|
+
elif cli_info["source"] == "package_manager":
|
|
84
|
+
display_info["package_manager"] = cli_info["package_manager"] or "unknown"
|
|
85
|
+
display_info["command"] = " ".join(cli_info["command"])
|
|
86
|
+
|
|
87
|
+
return display_info
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_cli_summary_table(cli_info_dict: dict[str, CLIInfo]) -> list[dict[str, str]]:
|
|
91
|
+
"""Create a table-ready summary of all CLI information.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
cli_info_dict: Dictionary of CLI name -> CLIInfo
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of formatted CLI info for table display
|
|
98
|
+
"""
|
|
99
|
+
return [
|
|
100
|
+
format_cli_info_for_display(cli_info) for cli_info in cli_info_dict.values()
|
|
101
|
+
]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Utilities for generating command line tools (curl, xh) from HTTP request data."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_curl_command(
|
|
9
|
+
method: str,
|
|
10
|
+
url: str,
|
|
11
|
+
headers: dict[str, str] | None = None,
|
|
12
|
+
body: Any = None,
|
|
13
|
+
is_json: bool = False,
|
|
14
|
+
pretty: bool = True,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Generate a curl command from HTTP request parameters.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
method: HTTP method (GET, POST, etc.)
|
|
20
|
+
url: Target URL
|
|
21
|
+
headers: HTTP headers dictionary
|
|
22
|
+
body: Request body (can be dict, str, bytes)
|
|
23
|
+
is_json: Whether the body should be treated as JSON
|
|
24
|
+
pretty: Whether to format the command for readability
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Complete curl command string
|
|
28
|
+
"""
|
|
29
|
+
parts = ["curl"]
|
|
30
|
+
|
|
31
|
+
# Add verbose flag for debugging
|
|
32
|
+
parts.append("-v")
|
|
33
|
+
|
|
34
|
+
# Add method if not GET
|
|
35
|
+
if method.upper() != "GET":
|
|
36
|
+
parts.extend(["-X", method.upper()])
|
|
37
|
+
|
|
38
|
+
# Add headers
|
|
39
|
+
if headers:
|
|
40
|
+
for key, value in headers.items():
|
|
41
|
+
parts.extend(["-H", f"{key}: {value}"])
|
|
42
|
+
|
|
43
|
+
# Add body
|
|
44
|
+
if isinstance(body, bytes):
|
|
45
|
+
body_str = body.decode()
|
|
46
|
+
parts.extend(["-d", body_str])
|
|
47
|
+
|
|
48
|
+
# Add URL (always last)
|
|
49
|
+
parts.append(url)
|
|
50
|
+
|
|
51
|
+
if pretty:
|
|
52
|
+
# Format for readability with line continuations
|
|
53
|
+
cmd_parts = []
|
|
54
|
+
for i, part in enumerate(parts):
|
|
55
|
+
if i == 0:
|
|
56
|
+
cmd_parts.append(part)
|
|
57
|
+
elif part in ["-X", "-H", "-d"]:
|
|
58
|
+
cmd_parts.append(f" \\\n {part}")
|
|
59
|
+
elif i == len(parts) - 1: # URL
|
|
60
|
+
cmd_parts.append(f" \\\n {shlex.quote(part)}")
|
|
61
|
+
else:
|
|
62
|
+
cmd_parts.append(f" {shlex.quote(part)}")
|
|
63
|
+
return "".join(cmd_parts)
|
|
64
|
+
else:
|
|
65
|
+
# Single line, properly quoted
|
|
66
|
+
return " ".join(shlex.quote(part) for part in parts)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def generate_xh_command(
|
|
70
|
+
method: str,
|
|
71
|
+
url: str,
|
|
72
|
+
headers: dict[str, str] | None = None,
|
|
73
|
+
body: Any = None,
|
|
74
|
+
is_json: bool = False,
|
|
75
|
+
pretty: bool = True,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Generate an xh (HTTPie-like) command from HTTP request parameters.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
method: HTTP method (GET, POST, etc.)
|
|
81
|
+
url: Target URL
|
|
82
|
+
headers: HTTP headers dictionary
|
|
83
|
+
body: Request body (can be dict, str, bytes)
|
|
84
|
+
is_json: Whether the body should be treated as JSON
|
|
85
|
+
pretty: Whether to format the command for readability
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Complete xh command string
|
|
89
|
+
"""
|
|
90
|
+
parts = ["xh"]
|
|
91
|
+
|
|
92
|
+
# Add verbose flag for debugging
|
|
93
|
+
parts.append("--verbose")
|
|
94
|
+
|
|
95
|
+
# Add method and URL
|
|
96
|
+
parts.append(f"{method.upper()}")
|
|
97
|
+
parts.append(url)
|
|
98
|
+
|
|
99
|
+
# Add headers
|
|
100
|
+
if headers:
|
|
101
|
+
for key, value in headers.items():
|
|
102
|
+
# Quote the entire header to handle special characters and spaces
|
|
103
|
+
parts.append(f"{key}:{value}")
|
|
104
|
+
|
|
105
|
+
# Add body
|
|
106
|
+
if isinstance(body, bytes):
|
|
107
|
+
body_str = body.decode()
|
|
108
|
+
parts.extend(["-d", body_str])
|
|
109
|
+
|
|
110
|
+
if pretty:
|
|
111
|
+
# Format for readability with line continuations
|
|
112
|
+
cmd_parts = []
|
|
113
|
+
for i, part in enumerate(parts):
|
|
114
|
+
if i == 0:
|
|
115
|
+
cmd_parts.append(part)
|
|
116
|
+
elif part == "--verbose" or i == 1:
|
|
117
|
+
cmd_parts.append(f" {part}")
|
|
118
|
+
elif i == 2: # URL
|
|
119
|
+
cmd_parts.append(f" \\\n {shlex.quote(part)}")
|
|
120
|
+
elif part in ("--raw", "-d"): # flags
|
|
121
|
+
cmd_parts.append(f" \\\n {part}")
|
|
122
|
+
elif ":" in part and not part.startswith("http"): # header
|
|
123
|
+
cmd_parts.append(f" \\\n {shlex.quote(part)}")
|
|
124
|
+
else:
|
|
125
|
+
cmd_parts.append(f" {shlex.quote(part)}")
|
|
126
|
+
return "".join(cmd_parts)
|
|
127
|
+
else:
|
|
128
|
+
# Single line, properly quoted
|
|
129
|
+
return " ".join(shlex.quote(part) for part in parts)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def generate_curl_shell_script(
|
|
133
|
+
method: str,
|
|
134
|
+
url: str,
|
|
135
|
+
headers: dict[str, str] | None = None,
|
|
136
|
+
body: Any = None,
|
|
137
|
+
is_json: bool = False,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""Generate a shell script with curl command using proper JSON handling.
|
|
140
|
+
|
|
141
|
+
This creates a more robust shell script that handles JSON safely by:
|
|
142
|
+
1. Storing JSON in a variable using heredoc or printf
|
|
143
|
+
2. Using the variable in the curl command
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
method: HTTP method (GET, POST, etc.)
|
|
147
|
+
url: Target URL
|
|
148
|
+
headers: HTTP headers dictionary
|
|
149
|
+
body: Request body (can be dict, str, bytes)
|
|
150
|
+
is_json: Whether the body should be treated as JSON
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Complete shell script content
|
|
154
|
+
"""
|
|
155
|
+
script_lines = ["#!/bin/bash", "set -e", ""]
|
|
156
|
+
|
|
157
|
+
# Process JSON body safely
|
|
158
|
+
json_data = None
|
|
159
|
+
if body is not None and (is_json or isinstance(body, dict)):
|
|
160
|
+
if isinstance(body, dict):
|
|
161
|
+
json_data = json.dumps(
|
|
162
|
+
body, indent=2, separators=(",", ": "), ensure_ascii=False
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
# Clean up string body
|
|
166
|
+
body_str = str(body)
|
|
167
|
+
if (body_str.startswith("b'") and body_str.endswith("'")) or (
|
|
168
|
+
body_str.startswith('b"') and body_str.endswith('"')
|
|
169
|
+
):
|
|
170
|
+
body_str = body_str[2:-1]
|
|
171
|
+
|
|
172
|
+
body_str = body_str.replace('\\"', '"').replace("\\'", "'")
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
parsed = json.loads(body_str)
|
|
176
|
+
json_data = json.dumps(
|
|
177
|
+
parsed, indent=2, separators=(",", ": "), ensure_ascii=False
|
|
178
|
+
)
|
|
179
|
+
except (json.JSONDecodeError, ValueError):
|
|
180
|
+
json_data = body_str
|
|
181
|
+
|
|
182
|
+
# Build curl command parts
|
|
183
|
+
curl_parts = ["curl", "-v"]
|
|
184
|
+
|
|
185
|
+
if method.upper() != "GET":
|
|
186
|
+
curl_parts.extend(["-X", shlex.quote(method.upper())])
|
|
187
|
+
|
|
188
|
+
# Add headers
|
|
189
|
+
if headers:
|
|
190
|
+
for key, value in headers.items():
|
|
191
|
+
curl_parts.extend(["-H", shlex.quote(f"{key}: {value}")])
|
|
192
|
+
|
|
193
|
+
# Handle JSON body with heredoc
|
|
194
|
+
if json_data:
|
|
195
|
+
script_lines.append("# JSON payload")
|
|
196
|
+
script_lines.append("JSON_DATA=$(cat <<'EOF'")
|
|
197
|
+
script_lines.append(json_data)
|
|
198
|
+
script_lines.append("EOF")
|
|
199
|
+
script_lines.append(")")
|
|
200
|
+
script_lines.append("")
|
|
201
|
+
|
|
202
|
+
curl_parts.extend(["-d", '"$JSON_DATA"'])
|
|
203
|
+
|
|
204
|
+
# Add content-type if not present
|
|
205
|
+
if not headers or not any(k.lower() == "content-type" for k in headers):
|
|
206
|
+
curl_parts.extend(["-H", shlex.quote("Content-Type: application/json")])
|
|
207
|
+
elif body is not None:
|
|
208
|
+
# Non-JSON body
|
|
209
|
+
curl_parts.extend(["-d", shlex.quote(str(body))])
|
|
210
|
+
|
|
211
|
+
# Add URL
|
|
212
|
+
curl_parts.append(shlex.quote(url))
|
|
213
|
+
|
|
214
|
+
# Build final command
|
|
215
|
+
script_lines.append("# Execute curl command")
|
|
216
|
+
script_lines.append(" ".join(curl_parts))
|
|
217
|
+
script_lines.append("")
|
|
218
|
+
|
|
219
|
+
return "\n".join(script_lines)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def format_command_output(
|
|
223
|
+
request_id: str,
|
|
224
|
+
curl_command: str,
|
|
225
|
+
xh_command: str,
|
|
226
|
+
provider: str | None = None,
|
|
227
|
+
) -> str:
|
|
228
|
+
"""Format the command output for logging.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
request_id: Request ID for correlation
|
|
232
|
+
curl_command: Generated curl command
|
|
233
|
+
xh_command: Generated xh command
|
|
234
|
+
provider: Provider name (optional)
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Formatted output string
|
|
238
|
+
"""
|
|
239
|
+
provider_info = f" ({provider})" if provider else ""
|
|
240
|
+
|
|
241
|
+
return f"""
|
|
242
|
+
🔄 Request Replay Commands{provider_info} [ID: {request_id}]
|
|
243
|
+
|
|
244
|
+
📋 curl:
|
|
245
|
+
{curl_command}
|
|
246
|
+
|
|
247
|
+
📋 xh:
|
|
248
|
+
{xh_command}
|
|
249
|
+
|
|
250
|
+
─────────────────────────────────────────────────────────────────────
|
|
251
|
+
"""
|