ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
ccproxy/api/routes/health.py
CHANGED
|
@@ -6,535 +6,42 @@ Implements modern health check patterns following 2024 best practices:
|
|
|
6
6
|
- /health: Detailed diagnostics (comprehensive status)
|
|
7
7
|
|
|
8
8
|
Follows IETF Health Check Response Format draft standard.
|
|
9
|
-
TODO: health endpoint Content-Type header to only return application/health+json per IETF spec
|
|
10
9
|
"""
|
|
11
10
|
|
|
12
|
-
import asyncio
|
|
13
|
-
import functools
|
|
14
|
-
import shutil
|
|
15
|
-
import time
|
|
16
11
|
from datetime import UTC, datetime
|
|
17
|
-
from enum import Enum
|
|
18
12
|
from typing import Any
|
|
19
13
|
|
|
20
14
|
from fastapi import APIRouter, Response, status
|
|
21
|
-
from
|
|
22
|
-
from structlog import get_logger
|
|
15
|
+
from fastapi.responses import JSONResponse
|
|
23
16
|
|
|
24
|
-
from ccproxy import __version__
|
|
25
|
-
from ccproxy.
|
|
26
|
-
from ccproxy.core.async_utils import patched_typing
|
|
27
|
-
from ccproxy.services.credentials import CredentialsManager
|
|
17
|
+
from ccproxy.core import __version__
|
|
18
|
+
from ccproxy.core.logging import get_logger
|
|
28
19
|
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class ClaudeCliStatus(str, Enum):
|
|
35
|
-
"""Claude CLI status enumeration."""
|
|
36
|
-
|
|
37
|
-
AVAILABLE = "available"
|
|
38
|
-
NOT_INSTALLED = "not_installed"
|
|
39
|
-
BINARY_FOUND_BUT_ERRORS = "binary_found_but_errors"
|
|
40
|
-
TIMEOUT = "timeout"
|
|
41
|
-
ERROR = "error"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class CodexCliStatus(str, Enum):
|
|
45
|
-
"""Codex CLI status enumeration."""
|
|
46
|
-
|
|
47
|
-
AVAILABLE = "available"
|
|
48
|
-
NOT_INSTALLED = "not_installed"
|
|
49
|
-
BINARY_FOUND_BUT_ERRORS = "binary_found_but_errors"
|
|
50
|
-
TIMEOUT = "timeout"
|
|
51
|
-
ERROR = "error"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class ClaudeCliInfo(BaseModel):
|
|
55
|
-
"""Claude CLI information with structured data."""
|
|
56
|
-
|
|
57
|
-
status: ClaudeCliStatus
|
|
58
|
-
version: str | None = None
|
|
59
|
-
binary_path: str | None = None
|
|
60
|
-
version_output: str | None = None
|
|
61
|
-
error: str | None = None
|
|
62
|
-
return_code: str | None = None
|
|
21
|
+
class HealthJSONResponse(JSONResponse):
|
|
22
|
+
media_type = "application/health+json"
|
|
63
23
|
|
|
64
24
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
binary_path: str | None = None
|
|
71
|
-
version_output: str | None = None
|
|
72
|
-
error: str | None = None
|
|
73
|
-
return_code: str | None = None
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# Cache for Claude CLI check results
|
|
77
|
-
_claude_cli_cache: tuple[float, tuple[str, dict[str, Any]]] | None = None
|
|
78
|
-
# Cache for Codex CLI check results
|
|
79
|
-
_codex_cli_cache: tuple[float, tuple[str, dict[str, Any]]] | None = None
|
|
80
|
-
_cache_ttl_seconds = 300 # Cache for 5 minutes
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
async def _check_oauth2_credentials() -> tuple[str, dict[str, Any]]:
|
|
84
|
-
"""Check OAuth2 credentials health status.
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
Tuple of (status, details) where status is 'pass'/'fail'/'warn'
|
|
88
|
-
Details include token metadata without exposing sensitive data
|
|
89
|
-
"""
|
|
90
|
-
try:
|
|
91
|
-
manager = CredentialsManager()
|
|
92
|
-
validation = await manager.validate()
|
|
93
|
-
|
|
94
|
-
if validation.valid and not validation.expired:
|
|
95
|
-
# Get token metadata without exposing sensitive information
|
|
96
|
-
credentials = validation.credentials
|
|
97
|
-
oauth_token = credentials.claude_ai_oauth if credentials else None
|
|
98
|
-
|
|
99
|
-
details = {
|
|
100
|
-
"auth_status": "valid",
|
|
101
|
-
"credentials_path": str(validation.path) if validation.path else None,
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if oauth_token:
|
|
105
|
-
details.update(
|
|
106
|
-
{
|
|
107
|
-
"expiration": oauth_token.expires_at_datetime.isoformat()
|
|
108
|
-
if oauth_token.expires_at_datetime
|
|
109
|
-
else None,
|
|
110
|
-
"subscription_type": oauth_token.subscription_type,
|
|
111
|
-
"expires_in_hours": str(
|
|
112
|
-
int(
|
|
113
|
-
(
|
|
114
|
-
oauth_token.expires_at_datetime - datetime.now(UTC)
|
|
115
|
-
).total_seconds()
|
|
116
|
-
/ 3600
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
if oauth_token.expires_at_datetime
|
|
120
|
-
else None,
|
|
121
|
-
}
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
return "pass", details
|
|
125
|
-
else:
|
|
126
|
-
# Handle expired credentials
|
|
127
|
-
credentials = validation.credentials
|
|
128
|
-
oauth_token = credentials.claude_ai_oauth if credentials else None
|
|
129
|
-
|
|
130
|
-
details = {
|
|
131
|
-
"auth_status": "expired" if validation.expired else "invalid",
|
|
132
|
-
"credentials_path": str(validation.path) if validation.path else None,
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if oauth_token and oauth_token.expires_at_datetime:
|
|
136
|
-
details.update(
|
|
137
|
-
{
|
|
138
|
-
"expiration": oauth_token.expires_at_datetime.isoformat(),
|
|
139
|
-
"subscription_type": oauth_token.subscription_type,
|
|
140
|
-
"expired_hours_ago": str(
|
|
141
|
-
int(
|
|
142
|
-
(
|
|
143
|
-
datetime.now(UTC) - oauth_token.expires_at_datetime
|
|
144
|
-
).total_seconds()
|
|
145
|
-
/ 3600
|
|
146
|
-
)
|
|
147
|
-
)
|
|
148
|
-
if validation.expired
|
|
149
|
-
else None,
|
|
150
|
-
}
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
return "warn", details
|
|
154
|
-
|
|
155
|
-
except CredentialsNotFoundError:
|
|
156
|
-
return "warn", {
|
|
157
|
-
"auth_status": "not_configured",
|
|
158
|
-
"error": "Claude credentials file not found",
|
|
159
|
-
"credentials_path": None,
|
|
160
|
-
}
|
|
161
|
-
except CredentialsExpiredError:
|
|
162
|
-
return "warn", {
|
|
163
|
-
"auth_status": "expired",
|
|
164
|
-
"error": "Claude credentials have expired",
|
|
165
|
-
}
|
|
166
|
-
except Exception as e:
|
|
167
|
-
return "fail", {
|
|
168
|
-
"auth_status": "error",
|
|
169
|
-
"error": f"Unexpected error: {str(e)}",
|
|
25
|
+
def _health_responses(description: str) -> dict[int | str, dict[str, Any]]:
|
|
26
|
+
return {
|
|
27
|
+
200: {
|
|
28
|
+
"description": description,
|
|
29
|
+
"content": {"application/health+json": {"schema": {"type": "object"}}},
|
|
170
30
|
}
|
|
31
|
+
}
|
|
171
32
|
|
|
172
33
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"""Get Claude CLI path with caching. Returns None if not found."""
|
|
176
|
-
return shutil.which("claude")
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _get_codex_cli_path() -> str | None:
|
|
180
|
-
"""Get Codex CLI path with caching. Returns None if not found."""
|
|
181
|
-
return shutil.which("codex")
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
async def check_claude_code() -> tuple[str, dict[str, Any]]:
|
|
185
|
-
"""Check Claude Code CLI installation and version by running 'claude --version'.
|
|
186
|
-
|
|
187
|
-
Results are cached for 5 minutes to avoid repeated subprocess calls.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
Tuple of (status, details) where status is 'pass'/'fail'/'warn'
|
|
191
|
-
Details include CLI version and binary path
|
|
192
|
-
"""
|
|
193
|
-
global _claude_cli_cache
|
|
194
|
-
|
|
195
|
-
# Check if we have a valid cached result
|
|
196
|
-
current_time = time.time()
|
|
197
|
-
if _claude_cli_cache is not None:
|
|
198
|
-
cache_time, cached_result = _claude_cli_cache
|
|
199
|
-
if current_time - cache_time < _cache_ttl_seconds:
|
|
200
|
-
logger.debug("claude_cli_check_cache_hit")
|
|
201
|
-
return cached_result
|
|
202
|
-
|
|
203
|
-
logger.debug("claude_cli_check_cache_miss")
|
|
204
|
-
|
|
205
|
-
# First check if claude binary exists in PATH (cached)
|
|
206
|
-
claude_path = _get_claude_cli_path()
|
|
207
|
-
|
|
208
|
-
if not claude_path:
|
|
209
|
-
result = (
|
|
210
|
-
"warn",
|
|
211
|
-
{
|
|
212
|
-
"installation_status": "not_found",
|
|
213
|
-
"cli_status": "not_installed",
|
|
214
|
-
"error": "Claude CLI binary not found in PATH",
|
|
215
|
-
"version": None,
|
|
216
|
-
"binary_path": None,
|
|
217
|
-
},
|
|
218
|
-
)
|
|
219
|
-
# Cache the result
|
|
220
|
-
_claude_cli_cache = (current_time, result)
|
|
221
|
-
return result
|
|
222
|
-
|
|
223
|
-
try:
|
|
224
|
-
# Run 'claude --version' to get actual version
|
|
225
|
-
process = await asyncio.create_subprocess_exec(
|
|
226
|
-
"claude",
|
|
227
|
-
"--version",
|
|
228
|
-
stdout=asyncio.subprocess.PIPE,
|
|
229
|
-
stderr=asyncio.subprocess.PIPE,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
stdout, stderr = await process.communicate()
|
|
233
|
-
|
|
234
|
-
if process.returncode == 0:
|
|
235
|
-
version_output = stdout.decode().strip()
|
|
236
|
-
# Extract version from output (e.g., "1.0.48 (Claude Code)" -> "1.0.48")
|
|
237
|
-
if version_output:
|
|
238
|
-
import re
|
|
239
|
-
|
|
240
|
-
# Try to find a version pattern (e.g., "1.0.48", "v2.1.0")
|
|
241
|
-
version_match = re.search(
|
|
242
|
-
r"\b(?:v)?(\d+\.\d+(?:\.\d+)?)\b", version_output
|
|
243
|
-
)
|
|
244
|
-
if version_match:
|
|
245
|
-
version = version_match.group(1)
|
|
246
|
-
else:
|
|
247
|
-
# Fallback: take the first part if no version pattern found
|
|
248
|
-
parts = version_output.split()
|
|
249
|
-
version = parts[0] if parts else "unknown"
|
|
250
|
-
else:
|
|
251
|
-
version = "unknown"
|
|
252
|
-
|
|
253
|
-
result = (
|
|
254
|
-
"pass",
|
|
255
|
-
{
|
|
256
|
-
"installation_status": "found",
|
|
257
|
-
"cli_status": "available",
|
|
258
|
-
"version": version,
|
|
259
|
-
"binary_path": claude_path,
|
|
260
|
-
"version_output": version_output,
|
|
261
|
-
},
|
|
262
|
-
)
|
|
263
|
-
# Cache the result
|
|
264
|
-
_claude_cli_cache = (current_time, result)
|
|
265
|
-
return result
|
|
266
|
-
else:
|
|
267
|
-
# Binary exists but --version failed
|
|
268
|
-
error_output = stderr.decode().strip() if stderr else "Unknown error"
|
|
269
|
-
result = (
|
|
270
|
-
"warn",
|
|
271
|
-
{
|
|
272
|
-
"installation_status": "found_with_issues",
|
|
273
|
-
"cli_status": "binary_found_but_errors",
|
|
274
|
-
"error": f"'claude --version' failed: {error_output}",
|
|
275
|
-
"version": None,
|
|
276
|
-
"binary_path": claude_path,
|
|
277
|
-
"return_code": str(process.returncode),
|
|
278
|
-
},
|
|
279
|
-
)
|
|
280
|
-
# Cache the result
|
|
281
|
-
_claude_cli_cache = (current_time, result)
|
|
282
|
-
return result
|
|
283
|
-
|
|
284
|
-
except TimeoutError:
|
|
285
|
-
result = (
|
|
286
|
-
"warn",
|
|
287
|
-
{
|
|
288
|
-
"installation_status": "found_with_issues",
|
|
289
|
-
"cli_status": "timeout",
|
|
290
|
-
"error": "Timeout running 'claude --version'",
|
|
291
|
-
"version": None,
|
|
292
|
-
"binary_path": claude_path,
|
|
293
|
-
},
|
|
294
|
-
)
|
|
295
|
-
# Cache the result
|
|
296
|
-
_claude_cli_cache = (current_time, result)
|
|
297
|
-
return result
|
|
298
|
-
except Exception as e:
|
|
299
|
-
result = (
|
|
300
|
-
"fail",
|
|
301
|
-
{
|
|
302
|
-
"installation_status": "error",
|
|
303
|
-
"cli_status": "error",
|
|
304
|
-
"error": f"Unexpected error running 'claude --version': {str(e)}",
|
|
305
|
-
"version": None,
|
|
306
|
-
"binary_path": claude_path,
|
|
307
|
-
},
|
|
308
|
-
)
|
|
309
|
-
# Cache the result
|
|
310
|
-
_claude_cli_cache = (current_time, result)
|
|
311
|
-
return result
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
async def get_claude_cli_info() -> ClaudeCliInfo:
|
|
315
|
-
"""Get Claude CLI information as a structured Pydantic model.
|
|
316
|
-
|
|
317
|
-
Returns:
|
|
318
|
-
ClaudeCliInfo: Structured information about Claude CLI installation and status
|
|
319
|
-
"""
|
|
320
|
-
cli_status, cli_details = await check_claude_code()
|
|
321
|
-
|
|
322
|
-
# Map the status to our enum values
|
|
323
|
-
if cli_status == "pass":
|
|
324
|
-
status_value = ClaudeCliStatus.AVAILABLE
|
|
325
|
-
elif cli_details.get("cli_status") == "not_installed":
|
|
326
|
-
status_value = ClaudeCliStatus.NOT_INSTALLED
|
|
327
|
-
elif cli_details.get("cli_status") == "binary_found_but_errors":
|
|
328
|
-
status_value = ClaudeCliStatus.BINARY_FOUND_BUT_ERRORS
|
|
329
|
-
elif cli_details.get("cli_status") == "timeout":
|
|
330
|
-
status_value = ClaudeCliStatus.TIMEOUT
|
|
331
|
-
else:
|
|
332
|
-
status_value = ClaudeCliStatus.ERROR
|
|
333
|
-
|
|
334
|
-
return ClaudeCliInfo(
|
|
335
|
-
status=status_value,
|
|
336
|
-
version=cli_details.get("version"),
|
|
337
|
-
binary_path=cli_details.get("binary_path"),
|
|
338
|
-
version_output=cli_details.get("version_output"),
|
|
339
|
-
error=cli_details.get("error"),
|
|
340
|
-
return_code=cli_details.get("return_code"),
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
async def check_codex_cli() -> tuple[str, dict[str, Any]]:
|
|
345
|
-
"""Check Codex CLI installation and version by running 'codex --version'.
|
|
346
|
-
Results are cached for 5 minutes to avoid repeated subprocess calls.
|
|
347
|
-
Returns:
|
|
348
|
-
Tuple of (status, details) where status is 'pass'/'fail'/'warn'
|
|
349
|
-
Details include CLI version and binary path
|
|
350
|
-
"""
|
|
351
|
-
global _codex_cli_cache
|
|
352
|
-
# Check if we have a valid cached result
|
|
353
|
-
current_time = time.time()
|
|
354
|
-
if _codex_cli_cache is not None:
|
|
355
|
-
cache_time, cached_result = _codex_cli_cache
|
|
356
|
-
if current_time - cache_time < _cache_ttl_seconds:
|
|
357
|
-
logger.debug("codex_cli_check_cache_hit")
|
|
358
|
-
return cached_result
|
|
359
|
-
|
|
360
|
-
logger.debug("codex_cli_check_cache_miss")
|
|
361
|
-
|
|
362
|
-
# First check if codex binary exists in PATH (cached)
|
|
363
|
-
codex_path = _get_codex_cli_path()
|
|
364
|
-
if not codex_path:
|
|
365
|
-
result = (
|
|
366
|
-
"warn",
|
|
367
|
-
{
|
|
368
|
-
"installation_status": "not_found",
|
|
369
|
-
"cli_status": "not_installed",
|
|
370
|
-
"error": "Codex CLI binary not found in PATH",
|
|
371
|
-
"version": None,
|
|
372
|
-
"binary_path": None,
|
|
373
|
-
},
|
|
374
|
-
)
|
|
375
|
-
# Cache the result
|
|
376
|
-
_codex_cli_cache = (current_time, result)
|
|
377
|
-
return result
|
|
378
|
-
|
|
379
|
-
try:
|
|
380
|
-
# Run 'codex --version' to get actual version
|
|
381
|
-
process = await asyncio.create_subprocess_exec(
|
|
382
|
-
"codex",
|
|
383
|
-
"--version",
|
|
384
|
-
stdout=asyncio.subprocess.PIPE,
|
|
385
|
-
stderr=asyncio.subprocess.PIPE,
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
stdout, stderr = await process.communicate()
|
|
389
|
-
|
|
390
|
-
if process.returncode == 0:
|
|
391
|
-
version_output = stdout.decode().strip()
|
|
392
|
-
# Extract version from output (e.g., "codex 0.21.0" -> "0.21.0")
|
|
393
|
-
if version_output:
|
|
394
|
-
import re
|
|
395
|
-
|
|
396
|
-
# Try to find a version pattern (e.g., "0.21.0", "v1.0.0")
|
|
397
|
-
version_match = re.search(
|
|
398
|
-
r"\b(?:v)?(\d+\.\d+(?:\.\d+)?)\b", version_output
|
|
399
|
-
)
|
|
400
|
-
if version_match:
|
|
401
|
-
version = version_match.group(1)
|
|
402
|
-
else:
|
|
403
|
-
# Fallback: take the last part if no version pattern found
|
|
404
|
-
parts = version_output.split()
|
|
405
|
-
version = parts[-1] if parts else "unknown"
|
|
406
|
-
else:
|
|
407
|
-
version = "unknown"
|
|
408
|
-
|
|
409
|
-
result = (
|
|
410
|
-
"pass",
|
|
411
|
-
{
|
|
412
|
-
"installation_status": "found",
|
|
413
|
-
"cli_status": "available",
|
|
414
|
-
"version": version,
|
|
415
|
-
"binary_path": codex_path,
|
|
416
|
-
"version_output": version_output,
|
|
417
|
-
},
|
|
418
|
-
)
|
|
419
|
-
# Cache the result
|
|
420
|
-
_codex_cli_cache = (current_time, result)
|
|
421
|
-
return result
|
|
422
|
-
else:
|
|
423
|
-
# Binary exists but --version failed
|
|
424
|
-
error_output = stderr.decode().strip() if stderr else "Unknown error"
|
|
425
|
-
result = (
|
|
426
|
-
"warn",
|
|
427
|
-
{
|
|
428
|
-
"installation_status": "found_with_issues",
|
|
429
|
-
"cli_status": "binary_found_but_errors",
|
|
430
|
-
"error": f"'codex --version' failed: {error_output}",
|
|
431
|
-
"version": None,
|
|
432
|
-
"binary_path": codex_path,
|
|
433
|
-
"return_code": str(process.returncode),
|
|
434
|
-
},
|
|
435
|
-
)
|
|
436
|
-
# Cache the result
|
|
437
|
-
_codex_cli_cache = (current_time, result)
|
|
438
|
-
return result
|
|
439
|
-
|
|
440
|
-
except TimeoutError:
|
|
441
|
-
result = (
|
|
442
|
-
"warn",
|
|
443
|
-
{
|
|
444
|
-
"installation_status": "found_with_issues",
|
|
445
|
-
"cli_status": "timeout",
|
|
446
|
-
"error": "Timeout running 'codex --version'",
|
|
447
|
-
"version": None,
|
|
448
|
-
"binary_path": codex_path,
|
|
449
|
-
},
|
|
450
|
-
)
|
|
451
|
-
# Cache the result
|
|
452
|
-
_codex_cli_cache = (current_time, result)
|
|
453
|
-
return result
|
|
454
|
-
|
|
455
|
-
except Exception as e:
|
|
456
|
-
result = (
|
|
457
|
-
"fail",
|
|
458
|
-
{
|
|
459
|
-
"installation_status": "error",
|
|
460
|
-
"cli_status": "error",
|
|
461
|
-
"error": f"Unexpected error running 'codex --version': {str(e)}",
|
|
462
|
-
"version": None,
|
|
463
|
-
"binary_path": codex_path,
|
|
464
|
-
},
|
|
465
|
-
)
|
|
466
|
-
# Cache the result
|
|
467
|
-
_codex_cli_cache = (current_time, result)
|
|
468
|
-
return result
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
async def get_codex_cli_info() -> CodexCliInfo:
|
|
472
|
-
"""Get Codex CLI information as a structured Pydantic model.
|
|
473
|
-
Returns:
|
|
474
|
-
CodexCliInfo: Structured information about Codex CLI installation and status
|
|
475
|
-
"""
|
|
476
|
-
cli_status, cli_details = await check_codex_cli()
|
|
477
|
-
|
|
478
|
-
# Map the status to our enum values
|
|
479
|
-
if cli_status == "pass":
|
|
480
|
-
status_value = CodexCliStatus.AVAILABLE
|
|
481
|
-
elif cli_details.get("cli_status") == "not_installed":
|
|
482
|
-
status_value = CodexCliStatus.NOT_INSTALLED
|
|
483
|
-
elif cli_details.get("cli_status") == "binary_found_but_errors":
|
|
484
|
-
status_value = CodexCliStatus.BINARY_FOUND_BUT_ERRORS
|
|
485
|
-
elif cli_details.get("cli_status") == "timeout":
|
|
486
|
-
status_value = CodexCliStatus.TIMEOUT
|
|
487
|
-
else:
|
|
488
|
-
status_value = CodexCliStatus.ERROR
|
|
489
|
-
|
|
490
|
-
return CodexCliInfo(
|
|
491
|
-
status=status_value,
|
|
492
|
-
version=cli_details.get("version"),
|
|
493
|
-
binary_path=cli_details.get("binary_path"),
|
|
494
|
-
version_output=cli_details.get("version_output"),
|
|
495
|
-
error=cli_details.get("error"),
|
|
496
|
-
return_code=cli_details.get("return_code"),
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
async def _check_claude_sdk() -> tuple[str, dict[str, Any]]:
|
|
501
|
-
"""Check Claude SDK installation and version.
|
|
502
|
-
|
|
503
|
-
Returns:
|
|
504
|
-
Tuple of (status, details) where status is 'pass'/'fail'/'warn'
|
|
505
|
-
Details include SDK version and availability
|
|
506
|
-
"""
|
|
507
|
-
try:
|
|
508
|
-
# Try to import Claude Code SDK
|
|
509
|
-
with patched_typing():
|
|
510
|
-
from claude_code_sdk import __version__ as sdk_version
|
|
511
|
-
|
|
512
|
-
return "pass", {
|
|
513
|
-
"installation_status": "found",
|
|
514
|
-
"sdk_status": "available",
|
|
515
|
-
"version": sdk_version,
|
|
516
|
-
"import_successful": True,
|
|
517
|
-
}
|
|
34
|
+
router = APIRouter(default_response_class=HealthJSONResponse)
|
|
35
|
+
logger = get_logger(__name__)
|
|
518
36
|
|
|
519
|
-
|
|
520
|
-
return "warn", {
|
|
521
|
-
"installation_status": "not_found",
|
|
522
|
-
"sdk_status": "not_installed",
|
|
523
|
-
"error": f"Claude SDK not available: {str(e)}",
|
|
524
|
-
"version": None,
|
|
525
|
-
"import_successful": False,
|
|
526
|
-
}
|
|
527
|
-
except Exception as e:
|
|
528
|
-
return "fail", {
|
|
529
|
-
"installation_status": "error",
|
|
530
|
-
"sdk_status": "error",
|
|
531
|
-
"error": f"Unexpected error checking SDK: {str(e)}",
|
|
532
|
-
"version": None,
|
|
533
|
-
"import_successful": False,
|
|
534
|
-
}
|
|
37
|
+
# Authentication and CLI health are managed by provider plugins; no core CLI checks
|
|
535
38
|
|
|
536
39
|
|
|
537
|
-
@router.get(
|
|
40
|
+
@router.get(
|
|
41
|
+
"/health/live",
|
|
42
|
+
response_class=HealthJSONResponse,
|
|
43
|
+
responses=_health_responses("Liveness probe result"),
|
|
44
|
+
)
|
|
538
45
|
async def liveness_probe(response: Response) -> dict[str, Any]:
|
|
539
46
|
"""Liveness probe for Kubernetes.
|
|
540
47
|
|
|
@@ -544,11 +51,10 @@ async def liveness_probe(response: Response) -> dict[str, Any]:
|
|
|
544
51
|
Returns:
|
|
545
52
|
Simple health status following IETF health check format
|
|
546
53
|
"""
|
|
547
|
-
# Add cache control headers as per best practices
|
|
548
54
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
549
55
|
response.headers["Content-Type"] = "application/health+json"
|
|
550
56
|
|
|
551
|
-
logger.debug("
|
|
57
|
+
logger.debug("liveness_probe_request")
|
|
552
58
|
|
|
553
59
|
return {
|
|
554
60
|
"status": "pass",
|
|
@@ -557,7 +63,11 @@ async def liveness_probe(response: Response) -> dict[str, Any]:
|
|
|
557
63
|
}
|
|
558
64
|
|
|
559
65
|
|
|
560
|
-
@router.get(
|
|
66
|
+
@router.get(
|
|
67
|
+
"/health/ready",
|
|
68
|
+
response_class=HealthJSONResponse,
|
|
69
|
+
responses=_health_responses("Readiness probe result"),
|
|
70
|
+
)
|
|
561
71
|
async def readiness_probe(response: Response) -> dict[str, Any]:
|
|
562
72
|
"""Readiness probe for Kubernetes.
|
|
563
73
|
|
|
@@ -567,143 +77,40 @@ async def readiness_probe(response: Response) -> dict[str, Any]:
|
|
|
567
77
|
Returns:
|
|
568
78
|
Readiness status with critical dependency checks
|
|
569
79
|
"""
|
|
570
|
-
# Add cache control headers
|
|
571
80
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
572
81
|
response.headers["Content-Type"] = "application/health+json"
|
|
573
82
|
|
|
574
|
-
logger.debug("
|
|
575
|
-
|
|
576
|
-
# Check OAuth credentials, CLI, and SDK separately
|
|
577
|
-
oauth_status, oauth_details = await _check_oauth2_credentials()
|
|
578
|
-
cli_status, cli_details = await check_claude_code()
|
|
579
|
-
codex_cli_status, codex_cli_details = await check_codex_cli()
|
|
580
|
-
sdk_status, sdk_details = await _check_claude_sdk()
|
|
581
|
-
|
|
582
|
-
# Service is ready if no check returns "fail"
|
|
583
|
-
# "warn" statuses (missing credentials/CLI/SDK) don't prevent readiness
|
|
584
|
-
if (
|
|
585
|
-
oauth_status == "fail"
|
|
586
|
-
or cli_status == "fail"
|
|
587
|
-
or codex_cli_status == "fail"
|
|
588
|
-
or sdk_status == "fail"
|
|
589
|
-
):
|
|
590
|
-
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
|
591
|
-
failed_components = []
|
|
592
|
-
|
|
593
|
-
if oauth_status == "fail":
|
|
594
|
-
failed_components.append("oauth2_credentials")
|
|
595
|
-
if cli_status == "fail":
|
|
596
|
-
failed_components.append("claude_cli")
|
|
597
|
-
if codex_cli_status == "fail":
|
|
598
|
-
failed_components.append("codex_cli")
|
|
599
|
-
if sdk_status == "fail":
|
|
600
|
-
failed_components.append("claude_sdk")
|
|
601
|
-
|
|
602
|
-
return {
|
|
603
|
-
"status": "fail",
|
|
604
|
-
"version": __version__,
|
|
605
|
-
"output": f"Critical dependency error: {', '.join(failed_components)}",
|
|
606
|
-
"checks": {
|
|
607
|
-
"oauth2_credentials": [
|
|
608
|
-
{
|
|
609
|
-
"status": oauth_status,
|
|
610
|
-
"output": oauth_details.get("error", "OAuth credentials error"),
|
|
611
|
-
}
|
|
612
|
-
],
|
|
613
|
-
"claude_cli": [
|
|
614
|
-
{
|
|
615
|
-
"status": cli_status,
|
|
616
|
-
"output": cli_details.get("error", "Claude CLI error"),
|
|
617
|
-
}
|
|
618
|
-
],
|
|
619
|
-
"codex_cli": [
|
|
620
|
-
{
|
|
621
|
-
"status": codex_cli_status,
|
|
622
|
-
"output": codex_cli_details.get("error", "Codex CLI error"),
|
|
623
|
-
}
|
|
624
|
-
],
|
|
625
|
-
"claude_sdk": [
|
|
626
|
-
{
|
|
627
|
-
"status": sdk_status,
|
|
628
|
-
"output": sdk_details.get("error", "Claude SDK error"),
|
|
629
|
-
}
|
|
630
|
-
],
|
|
631
|
-
},
|
|
632
|
-
}
|
|
83
|
+
logger.debug("readiness_probe_request")
|
|
633
84
|
|
|
85
|
+
# Core readiness only checks application availability; plugins provide their own health
|
|
634
86
|
return {
|
|
635
87
|
"status": "pass",
|
|
636
88
|
"version": __version__,
|
|
637
89
|
"output": "Service is ready to accept traffic",
|
|
638
|
-
"checks": {
|
|
639
|
-
"oauth2_credentials": [
|
|
640
|
-
{
|
|
641
|
-
"status": oauth_status,
|
|
642
|
-
"output": f"OAuth credentials: {oauth_details.get('auth_status', 'unknown')}",
|
|
643
|
-
}
|
|
644
|
-
],
|
|
645
|
-
"claude_cli": [
|
|
646
|
-
{
|
|
647
|
-
"status": cli_status,
|
|
648
|
-
"output": f"Claude CLI: {cli_details.get('cli_status', 'unknown')}",
|
|
649
|
-
}
|
|
650
|
-
],
|
|
651
|
-
"codex_cli": [
|
|
652
|
-
{
|
|
653
|
-
"status": codex_cli_status,
|
|
654
|
-
"output": f"Codex CLI: {codex_cli_details.get('cli_status', 'unknown')}",
|
|
655
|
-
}
|
|
656
|
-
],
|
|
657
|
-
"claude_sdk": [
|
|
658
|
-
{
|
|
659
|
-
"status": sdk_status,
|
|
660
|
-
"output": f"Claude SDK: {sdk_details.get('sdk_status', 'unknown')}",
|
|
661
|
-
}
|
|
662
|
-
],
|
|
663
|
-
},
|
|
664
90
|
}
|
|
665
91
|
|
|
666
92
|
|
|
667
|
-
@router.get(
|
|
93
|
+
@router.get(
|
|
94
|
+
"/health",
|
|
95
|
+
response_class=HealthJSONResponse,
|
|
96
|
+
responses=_health_responses("Detailed health diagnostics"),
|
|
97
|
+
)
|
|
668
98
|
async def detailed_health_check(response: Response) -> dict[str, Any]:
|
|
669
99
|
"""Comprehensive health check for diagnostics and monitoring.
|
|
670
100
|
|
|
671
|
-
Provides detailed status of
|
|
672
|
-
|
|
101
|
+
Provides detailed status of core service only. Provider/plugin-specific
|
|
102
|
+
health, including CLI availability, is reported by each plugin's health endpoint.
|
|
673
103
|
|
|
674
104
|
Returns:
|
|
675
105
|
Detailed health status following IETF health check format
|
|
676
106
|
"""
|
|
677
|
-
# Add cache control headers
|
|
678
107
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
679
108
|
response.headers["Content-Type"] = "application/health+json"
|
|
680
109
|
|
|
681
|
-
logger.debug("
|
|
110
|
+
logger.debug("detailed_health_check_request")
|
|
682
111
|
|
|
683
|
-
# Perform all health checks
|
|
684
|
-
oauth_status, oauth_details = await _check_oauth2_credentials()
|
|
685
|
-
cli_status, cli_details = await check_claude_code()
|
|
686
|
-
codex_cli_status, codex_cli_details = await check_codex_cli()
|
|
687
|
-
sdk_status, sdk_details = await _check_claude_sdk()
|
|
688
|
-
|
|
689
|
-
# Determine overall status - prioritize failures, then warnings
|
|
690
112
|
overall_status = "pass"
|
|
691
|
-
|
|
692
|
-
oauth_status == "fail"
|
|
693
|
-
or cli_status == "fail"
|
|
694
|
-
or codex_cli_status == "fail"
|
|
695
|
-
or sdk_status == "fail"
|
|
696
|
-
):
|
|
697
|
-
overall_status = "fail"
|
|
698
|
-
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
|
699
|
-
elif (
|
|
700
|
-
oauth_status == "warn"
|
|
701
|
-
or cli_status == "warn"
|
|
702
|
-
or codex_cli_status == "warn"
|
|
703
|
-
or sdk_status == "warn"
|
|
704
|
-
):
|
|
705
|
-
overall_status = "warn"
|
|
706
|
-
response.status_code = status.HTTP_200_OK
|
|
113
|
+
response.status_code = status.HTTP_200_OK
|
|
707
114
|
|
|
708
115
|
current_time = datetime.now(UTC).isoformat()
|
|
709
116
|
|
|
@@ -714,53 +121,13 @@ async def detailed_health_check(response: Response) -> dict[str, Any]:
|
|
|
714
121
|
"description": "CCProxy API Server",
|
|
715
122
|
"time": current_time,
|
|
716
123
|
"checks": {
|
|
717
|
-
"
|
|
718
|
-
{
|
|
719
|
-
"componentId": "oauth2-credentials",
|
|
720
|
-
"componentType": "authentication",
|
|
721
|
-
"status": oauth_status,
|
|
722
|
-
"time": current_time,
|
|
723
|
-
"output": f"OAuth2 credentials: {oauth_details.get('auth_status', 'unknown')}",
|
|
724
|
-
**oauth_details,
|
|
725
|
-
}
|
|
726
|
-
],
|
|
727
|
-
"claude_cli": [
|
|
728
|
-
{
|
|
729
|
-
"componentId": "claude-cli",
|
|
730
|
-
"componentType": "external_dependency",
|
|
731
|
-
"status": cli_status,
|
|
732
|
-
"time": current_time,
|
|
733
|
-
"output": f"Claude CLI: {cli_details.get('cli_status', 'unknown')}",
|
|
734
|
-
**cli_details,
|
|
735
|
-
}
|
|
736
|
-
],
|
|
737
|
-
"codex_cli": [
|
|
738
|
-
{
|
|
739
|
-
"componentId": "codex-cli",
|
|
740
|
-
"componentType": "external_dependency",
|
|
741
|
-
"status": codex_cli_status,
|
|
742
|
-
"time": current_time,
|
|
743
|
-
"output": f"Codex CLI: {codex_cli_details.get('cli_status', 'unknown')}",
|
|
744
|
-
**codex_cli_details,
|
|
745
|
-
}
|
|
746
|
-
],
|
|
747
|
-
"claude_sdk": [
|
|
748
|
-
{
|
|
749
|
-
"componentId": "claude-sdk",
|
|
750
|
-
"componentType": "python_package",
|
|
751
|
-
"status": sdk_status,
|
|
752
|
-
"time": current_time,
|
|
753
|
-
"output": f"Claude SDK: {sdk_details.get('sdk_status', 'unknown')}",
|
|
754
|
-
**sdk_details,
|
|
755
|
-
}
|
|
756
|
-
],
|
|
757
|
-
"proxy_service": [
|
|
124
|
+
"service_container": [
|
|
758
125
|
{
|
|
759
|
-
"componentId": "
|
|
126
|
+
"componentId": "service-container",
|
|
760
127
|
"componentType": "service",
|
|
761
128
|
"status": "pass",
|
|
762
129
|
"time": current_time,
|
|
763
|
-
"output": "
|
|
130
|
+
"output": "Service container operational",
|
|
764
131
|
"version": __version__,
|
|
765
132
|
}
|
|
766
133
|
],
|