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,437 @@
|
|
|
1
|
+
"""Centralized CLI detection service for all plugins.
|
|
2
|
+
|
|
3
|
+
This module provides a unified interface for detecting CLI binaries,
|
|
4
|
+
checking versions, and managing CLI-related state across all plugins.
|
|
5
|
+
It eliminates duplicate CLI detection logic by consolidating common patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any, NamedTuple
|
|
12
|
+
|
|
13
|
+
import structlog
|
|
14
|
+
|
|
15
|
+
from ccproxy.config.settings import Settings
|
|
16
|
+
from ccproxy.config.utils import get_ccproxy_cache_dir
|
|
17
|
+
from ccproxy.utils.binary_resolver import BinaryResolver, CLIInfo
|
|
18
|
+
from ccproxy.utils.caching import TTLCache
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = structlog.get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CLIDetectionResult(NamedTuple):
|
|
25
|
+
"""Result of CLI detection for a specific binary."""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
version: str | None
|
|
29
|
+
command: list[str] | None
|
|
30
|
+
is_available: bool
|
|
31
|
+
source: str # "path", "package_manager", "fallback", or "unknown"
|
|
32
|
+
package_manager: str | None = None
|
|
33
|
+
cached: bool = False
|
|
34
|
+
fallback_data: dict[str, Any] | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CLIDetectionService:
|
|
38
|
+
"""Centralized service for CLI detection across all plugins.
|
|
39
|
+
|
|
40
|
+
This service provides:
|
|
41
|
+
- Unified binary detection using BinaryResolver
|
|
42
|
+
- Version detection with caching
|
|
43
|
+
- Fallback data support for when CLI is not available
|
|
44
|
+
- Consistent logging and error handling
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self, settings: Settings, binary_resolver: BinaryResolver | None = None
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize the CLI detection service.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
settings: Application settings
|
|
54
|
+
binary_resolver: Optional binary resolver instance. If None, creates a new one.
|
|
55
|
+
"""
|
|
56
|
+
self.settings = settings
|
|
57
|
+
self.cache_dir = get_ccproxy_cache_dir()
|
|
58
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
# Use injected resolver or create from settings for backward compatibility
|
|
61
|
+
self.resolver = binary_resolver or BinaryResolver.from_settings(settings)
|
|
62
|
+
|
|
63
|
+
# Enhanced TTL cache for detection results (10 minute TTL)
|
|
64
|
+
self._detection_cache = TTLCache(maxsize=64, ttl=600.0)
|
|
65
|
+
|
|
66
|
+
# Separate cache for version info (longer TTL since versions change infrequently)
|
|
67
|
+
self._version_cache = TTLCache(maxsize=32, ttl=1800.0) # 30 minutes
|
|
68
|
+
|
|
69
|
+
async def detect_cli(
|
|
70
|
+
self,
|
|
71
|
+
binary_name: str,
|
|
72
|
+
package_name: str | None = None,
|
|
73
|
+
version_flag: str = "--version",
|
|
74
|
+
version_parser: Any | None = None,
|
|
75
|
+
fallback_data: dict[str, Any] | None = None,
|
|
76
|
+
cache_key: str | None = None,
|
|
77
|
+
) -> CLIDetectionResult:
|
|
78
|
+
"""Detect a CLI binary and its version.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
binary_name: Name of the binary to detect (e.g., "claude", "codex")
|
|
82
|
+
package_name: NPM package name if different from binary name
|
|
83
|
+
version_flag: Flag to get version (default: "--version")
|
|
84
|
+
version_parser: Optional callable to parse version output
|
|
85
|
+
fallback_data: Optional fallback data if CLI is not available
|
|
86
|
+
cache_key: Optional cache key (defaults to binary_name)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
CLIDetectionResult with detection information
|
|
90
|
+
"""
|
|
91
|
+
cache_key = cache_key or binary_name
|
|
92
|
+
|
|
93
|
+
# Check TTL cache first
|
|
94
|
+
cached_result = self._detection_cache.get(cache_key)
|
|
95
|
+
if cached_result is not None:
|
|
96
|
+
logger.debug(
|
|
97
|
+
"cli_detection_cached",
|
|
98
|
+
binary=binary_name,
|
|
99
|
+
version=cached_result.version,
|
|
100
|
+
available=cached_result.is_available,
|
|
101
|
+
cache_hit=True,
|
|
102
|
+
)
|
|
103
|
+
return cached_result # type: ignore[no-any-return]
|
|
104
|
+
|
|
105
|
+
# Try to detect the binary
|
|
106
|
+
result = self.resolver.find_binary(binary_name, package_name)
|
|
107
|
+
|
|
108
|
+
if result:
|
|
109
|
+
# Binary found - get version
|
|
110
|
+
version = await self._get_cli_version(
|
|
111
|
+
result.command, version_flag, version_parser
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Determine source
|
|
115
|
+
source = "path" if result.is_direct else "package_manager"
|
|
116
|
+
|
|
117
|
+
detection_result = CLIDetectionResult(
|
|
118
|
+
name=binary_name,
|
|
119
|
+
version=version,
|
|
120
|
+
command=result.command,
|
|
121
|
+
is_available=True,
|
|
122
|
+
source=source,
|
|
123
|
+
package_manager=result.package_manager,
|
|
124
|
+
cached=False,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
logger.debug(
|
|
128
|
+
"cli_detection_success",
|
|
129
|
+
binary=binary_name,
|
|
130
|
+
version=version,
|
|
131
|
+
source=source,
|
|
132
|
+
package_manager=result.package_manager,
|
|
133
|
+
command=result.command,
|
|
134
|
+
cached=cached_result is not None,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
elif fallback_data:
|
|
138
|
+
# Use fallback data
|
|
139
|
+
detection_result = CLIDetectionResult(
|
|
140
|
+
name=binary_name,
|
|
141
|
+
version=fallback_data.get("version", "unknown"),
|
|
142
|
+
command=None,
|
|
143
|
+
is_available=False,
|
|
144
|
+
source="fallback",
|
|
145
|
+
package_manager=None,
|
|
146
|
+
cached=False,
|
|
147
|
+
fallback_data=fallback_data,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
logger.warning(
|
|
151
|
+
"cli_detection_using_fallback",
|
|
152
|
+
binary=binary_name,
|
|
153
|
+
reason="CLI not found",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
# Not found and no fallback
|
|
158
|
+
detection_result = CLIDetectionResult(
|
|
159
|
+
name=binary_name,
|
|
160
|
+
version=None,
|
|
161
|
+
command=None,
|
|
162
|
+
is_available=False,
|
|
163
|
+
source="unknown",
|
|
164
|
+
package_manager=None,
|
|
165
|
+
cached=False,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
logger.error(
|
|
169
|
+
"cli_detection_failed",
|
|
170
|
+
binary=binary_name,
|
|
171
|
+
package=package_name,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Cache the result with TTL
|
|
175
|
+
self._detection_cache.set(cache_key, detection_result)
|
|
176
|
+
|
|
177
|
+
return detection_result
|
|
178
|
+
|
|
179
|
+
async def _get_cli_version(
|
|
180
|
+
self,
|
|
181
|
+
cli_command: list[str],
|
|
182
|
+
version_flag: str,
|
|
183
|
+
version_parser: Any | None = None,
|
|
184
|
+
) -> str | None:
|
|
185
|
+
"""Get CLI version by executing version command with caching.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
cli_command: Command list to execute CLI
|
|
189
|
+
version_flag: Flag to get version
|
|
190
|
+
version_parser: Optional callable to parse version output
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Version string if successful, None otherwise
|
|
194
|
+
"""
|
|
195
|
+
# Create cache key from command and flag
|
|
196
|
+
cache_key = f"version:{':'.join(cli_command)}:{version_flag}"
|
|
197
|
+
|
|
198
|
+
# Check version cache first (longer TTL since versions change infrequently)
|
|
199
|
+
cached_version = self._version_cache.get(cache_key)
|
|
200
|
+
if cached_version is not None:
|
|
201
|
+
logger.debug(
|
|
202
|
+
"cli_version_cached",
|
|
203
|
+
command=cli_command[0],
|
|
204
|
+
version=cached_version,
|
|
205
|
+
cache_hit=True,
|
|
206
|
+
)
|
|
207
|
+
return cached_version # type: ignore[no-any-return]
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# Prepare command with version flag
|
|
211
|
+
cmd = cli_command + [version_flag]
|
|
212
|
+
|
|
213
|
+
# Run command with timeout
|
|
214
|
+
process = await asyncio.create_subprocess_exec(
|
|
215
|
+
*cmd,
|
|
216
|
+
stdout=asyncio.subprocess.PIPE,
|
|
217
|
+
stderr=asyncio.subprocess.PIPE,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
|
221
|
+
|
|
222
|
+
version = None
|
|
223
|
+
if process.returncode == 0 and stdout:
|
|
224
|
+
version_output = stdout.decode().strip()
|
|
225
|
+
|
|
226
|
+
# Use custom parser if provided
|
|
227
|
+
if version_parser:
|
|
228
|
+
parsed = version_parser(version_output)
|
|
229
|
+
version = str(parsed) if parsed is not None else None
|
|
230
|
+
else:
|
|
231
|
+
# Default parsing logic
|
|
232
|
+
version = self._parse_version_output(version_output)
|
|
233
|
+
|
|
234
|
+
# Try stderr as some CLIs output version there
|
|
235
|
+
if not version and stderr:
|
|
236
|
+
version_output = stderr.decode().strip()
|
|
237
|
+
if version_parser:
|
|
238
|
+
parsed = version_parser(version_output)
|
|
239
|
+
version = str(parsed) if parsed is not None else None
|
|
240
|
+
else:
|
|
241
|
+
version = self._parse_version_output(version_output)
|
|
242
|
+
|
|
243
|
+
# Cache the version result (even if None)
|
|
244
|
+
self._version_cache.set(cache_key, version)
|
|
245
|
+
|
|
246
|
+
return version
|
|
247
|
+
|
|
248
|
+
except TimeoutError:
|
|
249
|
+
logger.debug("cli_version_timeout", command=cli_command)
|
|
250
|
+
# Cache timeout result briefly to avoid repeated attempts
|
|
251
|
+
self._version_cache.set(cache_key, None)
|
|
252
|
+
return None
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.debug("cli_version_error", command=cli_command, error=str(e))
|
|
255
|
+
# Cache error result briefly to avoid repeated attempts
|
|
256
|
+
self._version_cache.set(cache_key, None)
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
def _parse_version_output(self, output: str) -> str:
|
|
260
|
+
"""Parse version from CLI output using common patterns.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
output: Raw version command output
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Parsed version string
|
|
267
|
+
"""
|
|
268
|
+
# Handle various common formats
|
|
269
|
+
if "/" in output:
|
|
270
|
+
# Handle "tool/1.0.0" format
|
|
271
|
+
output = output.split("/")[-1]
|
|
272
|
+
|
|
273
|
+
if "(" in output:
|
|
274
|
+
# Handle "1.0.0 (Tool Name)" format
|
|
275
|
+
output = output.split("(")[0].strip()
|
|
276
|
+
|
|
277
|
+
# Extract version number pattern (e.g., "1.0.0", "v1.0.0")
|
|
278
|
+
version_pattern = r"v?(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)"
|
|
279
|
+
match = re.search(version_pattern, output)
|
|
280
|
+
if match:
|
|
281
|
+
return match.group(1)
|
|
282
|
+
|
|
283
|
+
# Return cleaned output if no pattern matches
|
|
284
|
+
return output.strip()
|
|
285
|
+
|
|
286
|
+
def load_cached_version(
|
|
287
|
+
self, binary_name: str, cache_file: str | None = None
|
|
288
|
+
) -> str | None:
|
|
289
|
+
"""Load cached version for a binary.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
binary_name: Name of the binary
|
|
293
|
+
cache_file: Optional cache file name
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Cached version string or None
|
|
297
|
+
"""
|
|
298
|
+
cache_file_name = cache_file or f"{binary_name}_version.json"
|
|
299
|
+
cache_path = self.cache_dir / cache_file_name
|
|
300
|
+
|
|
301
|
+
if not cache_path.exists():
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
with cache_path.open("r") as f:
|
|
306
|
+
data = json.load(f)
|
|
307
|
+
version = data.get("version")
|
|
308
|
+
return str(version) if version is not None else None
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.debug("cache_load_error", file=str(cache_path), error=str(e))
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
def save_cached_version(
|
|
314
|
+
self,
|
|
315
|
+
binary_name: str,
|
|
316
|
+
version: str,
|
|
317
|
+
cache_file: str | None = None,
|
|
318
|
+
additional_data: dict[str, Any] | None = None,
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Save version to cache.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
binary_name: Name of the binary
|
|
324
|
+
version: Version string to cache
|
|
325
|
+
cache_file: Optional cache file name
|
|
326
|
+
additional_data: Additional data to cache
|
|
327
|
+
"""
|
|
328
|
+
cache_file_name = cache_file or f"{binary_name}_version.json"
|
|
329
|
+
cache_path = self.cache_dir / cache_file_name
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
data = {"binary": binary_name, "version": version}
|
|
333
|
+
if additional_data:
|
|
334
|
+
data.update(additional_data)
|
|
335
|
+
|
|
336
|
+
with cache_path.open("w") as f:
|
|
337
|
+
json.dump(data, f, indent=2)
|
|
338
|
+
|
|
339
|
+
logger.debug("cache_saved", file=str(cache_path), version=version)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.warning("cache_save_error", file=str(cache_path), error=str(e))
|
|
342
|
+
|
|
343
|
+
def get_cli_info(self, binary_name: str) -> CLIInfo:
|
|
344
|
+
"""Get CLI information in standard format.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
binary_name: Name of the binary
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
CLIInfo dictionary with structured information
|
|
351
|
+
"""
|
|
352
|
+
# Check if we have cached detection result
|
|
353
|
+
cached_result = self._detection_cache.get(binary_name)
|
|
354
|
+
if cached_result is not None:
|
|
355
|
+
return CLIInfo(
|
|
356
|
+
name=cached_result.name,
|
|
357
|
+
version=cached_result.version,
|
|
358
|
+
source=cached_result.source,
|
|
359
|
+
path=cached_result.command[0] if cached_result.command else None,
|
|
360
|
+
command=cached_result.command or [],
|
|
361
|
+
package_manager=cached_result.package_manager,
|
|
362
|
+
is_available=cached_result.is_available,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Fall back to resolver
|
|
366
|
+
return self.resolver.get_cli_info(binary_name)
|
|
367
|
+
|
|
368
|
+
def clear_cache(self) -> None:
|
|
369
|
+
"""Clear all detection caches."""
|
|
370
|
+
self._detection_cache.clear()
|
|
371
|
+
self._version_cache.clear()
|
|
372
|
+
self.resolver.clear_cache()
|
|
373
|
+
logger.debug("cli_detection_cache_cleared")
|
|
374
|
+
|
|
375
|
+
def get_all_detected(self) -> dict[str, CLIDetectionResult]:
|
|
376
|
+
"""Get all detected CLI binaries.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Dictionary of binary name to detection result
|
|
380
|
+
"""
|
|
381
|
+
# Extract all cached results from TTLCache
|
|
382
|
+
results: dict[str, CLIDetectionResult] = {}
|
|
383
|
+
if hasattr(self._detection_cache, "_cache"):
|
|
384
|
+
for key, (result, _) in self._detection_cache._cache.items():
|
|
385
|
+
if isinstance(key, str) and isinstance(result, CLIDetectionResult):
|
|
386
|
+
results[key] = result
|
|
387
|
+
return results
|
|
388
|
+
|
|
389
|
+
async def detect_multiple(
|
|
390
|
+
self,
|
|
391
|
+
binaries: list[tuple[str, str | None]],
|
|
392
|
+
parallel: bool = True,
|
|
393
|
+
) -> dict[str, CLIDetectionResult]:
|
|
394
|
+
"""Detect multiple CLI binaries.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
binaries: List of (binary_name, package_name) tuples
|
|
398
|
+
parallel: Whether to detect in parallel
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Dictionary of binary name to detection result
|
|
402
|
+
"""
|
|
403
|
+
if parallel:
|
|
404
|
+
# Detect in parallel
|
|
405
|
+
tasks = [
|
|
406
|
+
self.detect_cli(binary_name, package_name)
|
|
407
|
+
for binary_name, package_name in binaries
|
|
408
|
+
]
|
|
409
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
410
|
+
|
|
411
|
+
detected: dict[str, CLIDetectionResult] = {}
|
|
412
|
+
for (binary_name, _), result in zip(binaries, results, strict=False):
|
|
413
|
+
if isinstance(result, Exception):
|
|
414
|
+
logger.error(
|
|
415
|
+
"cli_detection_error",
|
|
416
|
+
binary=binary_name,
|
|
417
|
+
error=str(result),
|
|
418
|
+
)
|
|
419
|
+
elif isinstance(result, CLIDetectionResult):
|
|
420
|
+
detected[binary_name] = result
|
|
421
|
+
|
|
422
|
+
return detected
|
|
423
|
+
else:
|
|
424
|
+
# Detect sequentially
|
|
425
|
+
detected = {}
|
|
426
|
+
for binary_name, package_name in binaries:
|
|
427
|
+
try:
|
|
428
|
+
result = await self.detect_cli(binary_name, package_name)
|
|
429
|
+
detected[binary_name] = result
|
|
430
|
+
except Exception as e:
|
|
431
|
+
logger.error(
|
|
432
|
+
"cli_detection_error",
|
|
433
|
+
binary=binary_name,
|
|
434
|
+
error=str(e),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return detected
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Proxy and SSL configuration management service."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = structlog.get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProxyConfiguration:
|
|
15
|
+
"""Manages proxy and SSL configuration from environment."""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
"""Initialize by reading environment variables.
|
|
19
|
+
|
|
20
|
+
- Calls _init_proxy_url()
|
|
21
|
+
- Calls _init_ssl_context()
|
|
22
|
+
- Caches configuration
|
|
23
|
+
"""
|
|
24
|
+
self._proxy_url = self._init_proxy_url()
|
|
25
|
+
self._ssl_verify = self._init_ssl_context()
|
|
26
|
+
|
|
27
|
+
if self._proxy_url:
|
|
28
|
+
logger.info("proxy_configuration_detected", proxy_url=self._proxy_url)
|
|
29
|
+
if isinstance(self._ssl_verify, str):
|
|
30
|
+
logger.info("custom_ca_bundle_configured", ca_bundle=self._ssl_verify)
|
|
31
|
+
elif not self._ssl_verify:
|
|
32
|
+
logger.warning("ssl_verification_disabled_not_recommended_for_production")
|
|
33
|
+
|
|
34
|
+
def _init_proxy_url(self) -> str | None:
|
|
35
|
+
"""Extract proxy URL from environment.
|
|
36
|
+
|
|
37
|
+
- Checks HTTPS_PROXY (highest priority)
|
|
38
|
+
- Falls back to ALL_PROXY
|
|
39
|
+
- Falls back to HTTP_PROXY
|
|
40
|
+
- Handles case variations
|
|
41
|
+
"""
|
|
42
|
+
# Check in order of priority
|
|
43
|
+
proxy_vars = [
|
|
44
|
+
"HTTPS_PROXY",
|
|
45
|
+
"https_proxy",
|
|
46
|
+
"ALL_PROXY",
|
|
47
|
+
"all_proxy",
|
|
48
|
+
"HTTP_PROXY",
|
|
49
|
+
"http_proxy",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
for var in proxy_vars:
|
|
53
|
+
proxy_url = os.getenv(var)
|
|
54
|
+
if proxy_url:
|
|
55
|
+
return proxy_url
|
|
56
|
+
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def _init_ssl_context(self) -> str | bool:
|
|
60
|
+
"""Configure SSL verification and CA bundle.
|
|
61
|
+
|
|
62
|
+
- Checks REQUESTS_CA_BUNDLE for custom CA
|
|
63
|
+
- Checks SSL_CERT_FILE as fallback
|
|
64
|
+
- Checks SSL_VERIFY for disabling (not recommended)
|
|
65
|
+
- Returns: path | True | False
|
|
66
|
+
"""
|
|
67
|
+
# Check for custom CA bundle
|
|
68
|
+
ca_bundle = os.getenv("REQUESTS_CA_BUNDLE") or os.getenv("SSL_CERT_FILE")
|
|
69
|
+
if ca_bundle:
|
|
70
|
+
ca_path = Path(ca_bundle)
|
|
71
|
+
if ca_path.exists() and ca_path.is_file():
|
|
72
|
+
return str(ca_path)
|
|
73
|
+
else:
|
|
74
|
+
logger.warning("ca_bundle_file_not_found", ca_bundle=ca_bundle)
|
|
75
|
+
|
|
76
|
+
# Check if SSL verification should be disabled
|
|
77
|
+
ssl_verify = os.getenv("SSL_VERIFY", "true").lower()
|
|
78
|
+
return ssl_verify not in ("false", "0", "no", "off")
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def proxy_url(self) -> str | None:
|
|
82
|
+
"""Get configured proxy URL if any."""
|
|
83
|
+
return self._proxy_url
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def ssl_verify(self) -> str | bool:
|
|
87
|
+
"""Get SSL verification setting."""
|
|
88
|
+
return self._ssl_verify
|
|
89
|
+
|
|
90
|
+
def get_httpx_client_config(self) -> dict[str, Any]:
|
|
91
|
+
"""Build configuration dict for httpx.AsyncClient.
|
|
92
|
+
|
|
93
|
+
- Includes 'proxy' if proxy configured
|
|
94
|
+
- Includes 'verify' for SSL settings
|
|
95
|
+
- Ready to pass to client constructor
|
|
96
|
+
"""
|
|
97
|
+
config = {
|
|
98
|
+
"verify": self._ssl_verify,
|
|
99
|
+
"timeout": 120.0, # Default timeout
|
|
100
|
+
"follow_redirects": False,
|
|
101
|
+
"limits": httpx.Limits(
|
|
102
|
+
max_keepalive_connections=100,
|
|
103
|
+
max_connections=1000,
|
|
104
|
+
keepalive_expiry=30.0,
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if self._proxy_url:
|
|
109
|
+
config["proxy"] = self._proxy_url
|
|
110
|
+
|
|
111
|
+
return config
|