ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
"""OAuth provider implementation for GitHub Copilot."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from ccproxy.auth.managers.token_snapshot import TokenSnapshot
|
|
9
|
+
from ccproxy.auth.oauth.protocol import ProfileLoggingMixin, StandardProfileFields
|
|
10
|
+
from ccproxy.auth.oauth.registry import CliAuthConfig, FlowType, OAuthProviderInfo
|
|
11
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
12
|
+
|
|
13
|
+
from ..config import CopilotOAuthConfig
|
|
14
|
+
from .client import CopilotOAuthClient
|
|
15
|
+
from .models import (
|
|
16
|
+
CopilotCredentials,
|
|
17
|
+
CopilotOAuthToken,
|
|
18
|
+
CopilotTokenInfo,
|
|
19
|
+
CopilotTokenResponse,
|
|
20
|
+
)
|
|
21
|
+
from .storage import CopilotOAuthStorage
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from ccproxy.services.cli_detection import CLIDetectionService
|
|
26
|
+
|
|
27
|
+
from ..manager import CopilotTokenManager
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = get_plugin_logger()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CopilotOAuthProvider(ProfileLoggingMixin):
|
|
34
|
+
"""GitHub Copilot OAuth provider implementation."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
config: CopilotOAuthConfig | None = None,
|
|
39
|
+
storage: CopilotOAuthStorage | None = None,
|
|
40
|
+
http_client: httpx.AsyncClient | None = None,
|
|
41
|
+
hook_manager: Any | None = None,
|
|
42
|
+
detection_service: "CLIDetectionService | None" = None,
|
|
43
|
+
):
|
|
44
|
+
"""Initialize Copilot OAuth provider.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
config: OAuth configuration
|
|
48
|
+
storage: Token storage
|
|
49
|
+
http_client: Optional HTTP client for request tracing
|
|
50
|
+
hook_manager: Optional hook manager for events
|
|
51
|
+
detection_service: Optional CLI detection service
|
|
52
|
+
"""
|
|
53
|
+
self.config = config or CopilotOAuthConfig()
|
|
54
|
+
self.storage = storage or CopilotOAuthStorage()
|
|
55
|
+
self.hook_manager = hook_manager
|
|
56
|
+
self.detection_service = detection_service
|
|
57
|
+
self.http_client = http_client
|
|
58
|
+
self._cached_profile: StandardProfileFields | None = None
|
|
59
|
+
|
|
60
|
+
self.client = CopilotOAuthClient(
|
|
61
|
+
self.config,
|
|
62
|
+
self.storage,
|
|
63
|
+
http_client,
|
|
64
|
+
hook_manager=hook_manager,
|
|
65
|
+
detection_service=detection_service,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def provider_name(self) -> str:
|
|
70
|
+
"""Internal provider name."""
|
|
71
|
+
return "copilot"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def provider_display_name(self) -> str:
|
|
75
|
+
"""Display name for UI."""
|
|
76
|
+
return "GitHub Copilot"
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def supports_pkce(self) -> bool:
|
|
80
|
+
"""Whether this provider supports PKCE."""
|
|
81
|
+
return self.config.use_pkce
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def supports_refresh(self) -> bool:
|
|
85
|
+
"""Whether this provider supports token refresh."""
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def requires_client_secret(self) -> bool:
|
|
90
|
+
"""Whether this provider requires a client secret."""
|
|
91
|
+
return False # GitHub Device Code Flow doesn't require client secret
|
|
92
|
+
|
|
93
|
+
async def get_authorization_url(
|
|
94
|
+
self,
|
|
95
|
+
state: str,
|
|
96
|
+
code_verifier: str | None = None,
|
|
97
|
+
redirect_uri: str | None = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Get the authorization URL for GitHub Device Code Flow.
|
|
100
|
+
|
|
101
|
+
For device code flow, this returns the device authorization endpoint.
|
|
102
|
+
The actual user verification happens at the verification_uri returned
|
|
103
|
+
by start_device_flow().
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
state: OAuth state parameter (not used in device flow)
|
|
107
|
+
code_verifier: PKCE code verifier (not used in device flow)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Device authorization URL
|
|
111
|
+
"""
|
|
112
|
+
# For device code flow, we return the device authorization endpoint
|
|
113
|
+
# The actual flow is handled by the device flow methods
|
|
114
|
+
return self.config.authorize_url
|
|
115
|
+
|
|
116
|
+
async def start_device_flow(self) -> tuple[str, str, str, int]:
|
|
117
|
+
"""Start the GitHub device code authorization flow.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Tuple of (device_code, user_code, verification_uri, expires_in)
|
|
121
|
+
"""
|
|
122
|
+
device_response = await self.client.start_device_flow()
|
|
123
|
+
|
|
124
|
+
logger.info(
|
|
125
|
+
"device_flow_started",
|
|
126
|
+
user_code=device_response.user_code,
|
|
127
|
+
verification_uri=device_response.verification_uri,
|
|
128
|
+
expires_in=device_response.expires_in,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
device_response.device_code,
|
|
133
|
+
device_response.user_code,
|
|
134
|
+
device_response.verification_uri,
|
|
135
|
+
device_response.expires_in,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
async def complete_device_flow(
|
|
139
|
+
self, device_code: str, interval: int = 5, expires_in: int = 900
|
|
140
|
+
) -> CopilotCredentials:
|
|
141
|
+
"""Complete the device flow authorization.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
device_code: Device code from start_device_flow
|
|
145
|
+
interval: Polling interval in seconds
|
|
146
|
+
expires_in: Code expiration time in seconds
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Complete Copilot credentials
|
|
150
|
+
"""
|
|
151
|
+
return await self.client.complete_authorization(
|
|
152
|
+
device_code, interval, expires_in
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def handle_callback(
|
|
156
|
+
self,
|
|
157
|
+
code: str,
|
|
158
|
+
state: str,
|
|
159
|
+
code_verifier: str | None = None,
|
|
160
|
+
redirect_uri: str | None = None,
|
|
161
|
+
) -> Any:
|
|
162
|
+
"""Handle OAuth callback (not used in device flow).
|
|
163
|
+
|
|
164
|
+
This method is required by the CLI flow protocol but not used for
|
|
165
|
+
device code flow. Use complete_device_flow instead.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
code: Authorization code from OAuth callback
|
|
169
|
+
state: State parameter for validation
|
|
170
|
+
code_verifier: PKCE code verifier (if PKCE is used)
|
|
171
|
+
redirect_uri: Redirect URI used in authorization (optional)
|
|
172
|
+
"""
|
|
173
|
+
raise NotImplementedError(
|
|
174
|
+
"Copilot uses device code flow. Browser callback is not supported."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def exchange_code(
|
|
178
|
+
self, code: str, state: str, code_verifier: str | None = None
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
"""Exchange authorization code for token (not used in device flow).
|
|
181
|
+
|
|
182
|
+
This method is required by the OAuth protocol but not used for
|
|
183
|
+
device code flow. Use complete_device_flow instead.
|
|
184
|
+
"""
|
|
185
|
+
raise NotImplementedError(
|
|
186
|
+
"Device code flow doesn't use authorization code exchange. "
|
|
187
|
+
"Use complete_device_flow instead."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def refresh_token(self, refresh_token: str) -> dict[str, Any]:
|
|
191
|
+
"""Refresh access token using refresh token.
|
|
192
|
+
|
|
193
|
+
For Copilot, this refreshes the Copilot service token using the
|
|
194
|
+
stored OAuth token.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
refresh_token: Not used for Copilot (uses OAuth token instead)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Token information
|
|
201
|
+
"""
|
|
202
|
+
credentials = await self.storage.load_credentials()
|
|
203
|
+
if not credentials:
|
|
204
|
+
raise ValueError("No credentials found for refresh")
|
|
205
|
+
|
|
206
|
+
refreshed_credentials = await self.client.refresh_copilot_token(credentials)
|
|
207
|
+
|
|
208
|
+
# Return token info in standard format
|
|
209
|
+
if refreshed_credentials.copilot_token is not None:
|
|
210
|
+
return {
|
|
211
|
+
"access_token": refreshed_credentials.copilot_token.token.get_secret_value(),
|
|
212
|
+
"token_type": "bearer",
|
|
213
|
+
"expires_at": refreshed_credentials.copilot_token.expires_at,
|
|
214
|
+
"provider": self.provider_name,
|
|
215
|
+
}
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError("Failed to refresh Copilot token")
|
|
218
|
+
|
|
219
|
+
async def get_user_profile(
|
|
220
|
+
self, access_token: str | None = None
|
|
221
|
+
) -> StandardProfileFields:
|
|
222
|
+
"""Get user profile information.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
access_token: Optional OAuth access token (not Copilot token)
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
User profile information
|
|
229
|
+
"""
|
|
230
|
+
oauth_token: CopilotOAuthToken | None = None
|
|
231
|
+
|
|
232
|
+
if access_token:
|
|
233
|
+
from pydantic import SecretStr
|
|
234
|
+
|
|
235
|
+
oauth_token = CopilotOAuthToken(
|
|
236
|
+
access_token=SecretStr(access_token), expires_in=None, created_at=None
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
credentials = await self.storage.load_credentials()
|
|
240
|
+
if not credentials:
|
|
241
|
+
raise ValueError("No credentials found")
|
|
242
|
+
oauth_token = credentials.oauth_token
|
|
243
|
+
|
|
244
|
+
profile = await self.client.get_standard_profile(oauth_token)
|
|
245
|
+
self._cached_profile = profile
|
|
246
|
+
return profile
|
|
247
|
+
|
|
248
|
+
async def get_standard_profile(
|
|
249
|
+
self, credentials: Any | None = None
|
|
250
|
+
) -> StandardProfileFields | None:
|
|
251
|
+
"""Get standardized profile information from credentials.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
credentials: Copilot credentials object (optional)
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Standardized profile fields or None if not available
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
# If credentials is None, try to load from storage
|
|
261
|
+
if credentials is None:
|
|
262
|
+
try:
|
|
263
|
+
credentials = await self.storage.load_credentials()
|
|
264
|
+
if not credentials:
|
|
265
|
+
return None
|
|
266
|
+
except Exception:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
# If credentials has OAuth token, use it directly
|
|
270
|
+
if hasattr(credentials, "oauth_token") and credentials.oauth_token:
|
|
271
|
+
return await self.client.get_standard_profile(credentials.oauth_token)
|
|
272
|
+
else:
|
|
273
|
+
# Fallback to loading from storage
|
|
274
|
+
return await self.get_user_profile()
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.debug(
|
|
277
|
+
"get_standard_profile_failed",
|
|
278
|
+
error=str(e),
|
|
279
|
+
exc_info=e,
|
|
280
|
+
)
|
|
281
|
+
# Return fallback profile using _extract_standard_profile if we have credentials
|
|
282
|
+
if credentials is not None:
|
|
283
|
+
return self._extract_standard_profile(credentials)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
async def get_copilot_token_data(self) -> CopilotTokenResponse | None:
|
|
287
|
+
credentials = await self.storage.load_credentials()
|
|
288
|
+
if not credentials:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
return credentials.copilot_token
|
|
292
|
+
|
|
293
|
+
async def get_token_info(self) -> CopilotTokenInfo | None:
|
|
294
|
+
"""Get current token information.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Token information if available
|
|
298
|
+
"""
|
|
299
|
+
credentials = await self.storage.load_credentials()
|
|
300
|
+
if not credentials:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
oauth_expires_at = credentials.oauth_token.expires_at_datetime
|
|
304
|
+
copilot_expires_at = None
|
|
305
|
+
|
|
306
|
+
if credentials.copilot_token and credentials.copilot_token.expires_at:
|
|
307
|
+
# expires_at is now a datetime object, no need to parse
|
|
308
|
+
copilot_expires_at = credentials.copilot_token.expires_at
|
|
309
|
+
|
|
310
|
+
# Get profile for additional info
|
|
311
|
+
profile = None
|
|
312
|
+
with contextlib.suppress(Exception):
|
|
313
|
+
profile = await self.get_user_profile()
|
|
314
|
+
|
|
315
|
+
copilot_access = False
|
|
316
|
+
if profile is not None:
|
|
317
|
+
features = getattr(profile, "features", {}) or {}
|
|
318
|
+
copilot_access = bool(features.get("copilot_access"))
|
|
319
|
+
if not copilot_access and getattr(profile, "subscription_type", None):
|
|
320
|
+
copilot_access = True
|
|
321
|
+
|
|
322
|
+
if not copilot_access and credentials.copilot_token is not None:
|
|
323
|
+
token = credentials.copilot_token
|
|
324
|
+
indicative_flags = [
|
|
325
|
+
getattr(token, "chat_enabled", None),
|
|
326
|
+
getattr(token, "annotations_enabled", None),
|
|
327
|
+
getattr(token, "individual", None),
|
|
328
|
+
]
|
|
329
|
+
if any(flag is True for flag in indicative_flags if flag is not None):
|
|
330
|
+
copilot_access = True
|
|
331
|
+
else:
|
|
332
|
+
copilot_access = (
|
|
333
|
+
True # Possession of a copilot token implies active access
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if not copilot_access:
|
|
337
|
+
copilot_access = credentials.copilot_token is not None
|
|
338
|
+
|
|
339
|
+
return CopilotTokenInfo(
|
|
340
|
+
provider="copilot",
|
|
341
|
+
oauth_expires_at=oauth_expires_at,
|
|
342
|
+
copilot_expires_at=copilot_expires_at,
|
|
343
|
+
account_type=credentials.account_type,
|
|
344
|
+
copilot_access=copilot_access,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
async def get_token_snapshot(self) -> TokenSnapshot | None:
|
|
348
|
+
"""Return a token snapshot built from stored credentials."""
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
manager = await self.create_token_manager(storage=self.storage)
|
|
352
|
+
snapshot = await manager.get_token_snapshot()
|
|
353
|
+
if snapshot:
|
|
354
|
+
return snapshot
|
|
355
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
356
|
+
logger.debug("copilot_snapshot_via_manager_failed", error=str(exc))
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
credentials = await self.storage.load_credentials()
|
|
360
|
+
if not credentials:
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
from ..manager import CopilotTokenManager
|
|
364
|
+
|
|
365
|
+
temp_manager = CopilotTokenManager(storage=self.storage)
|
|
366
|
+
return temp_manager._build_token_snapshot(credentials)
|
|
367
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
368
|
+
logger.debug("copilot_snapshot_from_credentials_failed", error=str(exc))
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
async def is_authenticated(self) -> bool:
|
|
372
|
+
"""Check if user is authenticated with valid tokens.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
True if authenticated with valid tokens
|
|
376
|
+
"""
|
|
377
|
+
credentials = await self.storage.load_credentials()
|
|
378
|
+
if not credentials:
|
|
379
|
+
return False
|
|
380
|
+
|
|
381
|
+
# Check if OAuth token is expired
|
|
382
|
+
if credentials.oauth_token.is_expired:
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
# Check if we have a valid (non-expired) Copilot token
|
|
386
|
+
if not credentials.copilot_token:
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
# Check if Copilot token is expired
|
|
390
|
+
return not credentials.copilot_token.is_expired
|
|
391
|
+
|
|
392
|
+
async def get_copilot_token(self) -> str | None:
|
|
393
|
+
"""Get current Copilot service token for API requests.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Copilot token if available and valid, None otherwise
|
|
397
|
+
"""
|
|
398
|
+
credentials = await self.storage.load_credentials()
|
|
399
|
+
if not credentials or not credentials.copilot_token:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
# Check if token is expired
|
|
403
|
+
if credentials.copilot_token.is_expired:
|
|
404
|
+
logger.info(
|
|
405
|
+
"copilot_token_expired_in_get",
|
|
406
|
+
expires_at=credentials.copilot_token.expires_at,
|
|
407
|
+
)
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
return credentials.copilot_token.token.get_secret_value()
|
|
411
|
+
|
|
412
|
+
async def ensure_oauth_token(self) -> str:
|
|
413
|
+
"""Ensure we have a valid OAuth token.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Valid OAuth token
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
ValueError: If unable to get valid token
|
|
420
|
+
"""
|
|
421
|
+
credentials = await self.storage.load_credentials()
|
|
422
|
+
if not credentials:
|
|
423
|
+
raise ValueError("No credentials found - authorization required")
|
|
424
|
+
|
|
425
|
+
if credentials.oauth_token.is_expired:
|
|
426
|
+
raise ValueError("OAuth token expired - re-authorization required")
|
|
427
|
+
|
|
428
|
+
return credentials.oauth_token.access_token.get_secret_value()
|
|
429
|
+
|
|
430
|
+
async def logout(self) -> None:
|
|
431
|
+
"""Clear stored credentials."""
|
|
432
|
+
await self.storage.clear_credentials()
|
|
433
|
+
|
|
434
|
+
def get_storage(self) -> Any:
|
|
435
|
+
"""Get storage implementation for this provider.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Storage implementation
|
|
439
|
+
"""
|
|
440
|
+
return self.storage
|
|
441
|
+
|
|
442
|
+
async def load_credentials(self, custom_path: Any | None = None) -> Any | None:
|
|
443
|
+
"""Load credentials from provider's storage.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
custom_path: Optional custom storage path (Path object)
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Credentials if found, None otherwise
|
|
450
|
+
"""
|
|
451
|
+
try:
|
|
452
|
+
if custom_path:
|
|
453
|
+
# Create storage with custom path
|
|
454
|
+
from pathlib import Path
|
|
455
|
+
|
|
456
|
+
from .storage import CopilotOAuthStorage
|
|
457
|
+
|
|
458
|
+
storage = CopilotOAuthStorage(credentials_path=Path(custom_path))
|
|
459
|
+
credentials = await storage.load_credentials()
|
|
460
|
+
else:
|
|
461
|
+
# Load from default storage
|
|
462
|
+
credentials = await self.storage.load_credentials()
|
|
463
|
+
|
|
464
|
+
# Use standardized profile logging
|
|
465
|
+
self._log_credentials_loaded("copilot", credentials)
|
|
466
|
+
|
|
467
|
+
return credentials
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.debug(
|
|
470
|
+
"copilot_load_credentials_failed",
|
|
471
|
+
error=str(e),
|
|
472
|
+
exc_info=e,
|
|
473
|
+
)
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
async def save_credentials(self, credentials: CopilotCredentials | None) -> bool:
|
|
477
|
+
"""Save credentials to storage.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
credentials: Copilot credentials to save (None to clear)
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
True if save was successful
|
|
484
|
+
"""
|
|
485
|
+
try:
|
|
486
|
+
if credentials is None:
|
|
487
|
+
await self.storage.clear_credentials()
|
|
488
|
+
logger.info("copilot_credentials_cleared")
|
|
489
|
+
return True
|
|
490
|
+
else:
|
|
491
|
+
await self.storage.save_credentials(credentials)
|
|
492
|
+
logger.info(
|
|
493
|
+
"copilot_credentials_saved",
|
|
494
|
+
account_type=credentials.account_type,
|
|
495
|
+
has_oauth=bool(credentials.oauth_token),
|
|
496
|
+
has_copilot_token=bool(credentials.copilot_token),
|
|
497
|
+
)
|
|
498
|
+
return True
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(
|
|
501
|
+
"copilot_credentials_save_failed",
|
|
502
|
+
error=str(e),
|
|
503
|
+
exc_info=e,
|
|
504
|
+
)
|
|
505
|
+
return False
|
|
506
|
+
|
|
507
|
+
async def create_token_manager(
|
|
508
|
+
self, storage: Any | None = None
|
|
509
|
+
) -> "CopilotTokenManager":
|
|
510
|
+
"""Create a token manager instance wired to this provider's context."""
|
|
511
|
+
|
|
512
|
+
from ..manager import CopilotTokenManager
|
|
513
|
+
|
|
514
|
+
return await CopilotTokenManager.create(
|
|
515
|
+
storage=storage or self.storage,
|
|
516
|
+
config=self.config,
|
|
517
|
+
http_client=self.http_client,
|
|
518
|
+
hook_manager=self.hook_manager,
|
|
519
|
+
detection_service=self.detection_service,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def _extract_standard_profile(self, credentials: Any) -> StandardProfileFields:
|
|
523
|
+
"""Extract standardized profile fields from Copilot credentials."""
|
|
524
|
+
from .models import CopilotCredentials, CopilotProfileInfo
|
|
525
|
+
|
|
526
|
+
if isinstance(credentials, CopilotProfileInfo):
|
|
527
|
+
return StandardProfileFields(
|
|
528
|
+
account_id=credentials.account_id,
|
|
529
|
+
provider_type="copilot",
|
|
530
|
+
email=credentials.email,
|
|
531
|
+
display_name=credentials.name or credentials.login,
|
|
532
|
+
)
|
|
533
|
+
elif isinstance(credentials, CopilotCredentials):
|
|
534
|
+
# Fallback for when we only have credentials without profile
|
|
535
|
+
return StandardProfileFields(
|
|
536
|
+
account_id="unknown",
|
|
537
|
+
provider_type="copilot",
|
|
538
|
+
email=None,
|
|
539
|
+
display_name="GitHub Copilot User",
|
|
540
|
+
)
|
|
541
|
+
else:
|
|
542
|
+
return StandardProfileFields(
|
|
543
|
+
account_id="unknown",
|
|
544
|
+
provider_type="copilot",
|
|
545
|
+
email=None,
|
|
546
|
+
display_name="Unknown User",
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
async def cleanup(self) -> None:
|
|
550
|
+
"""Cleanup resources."""
|
|
551
|
+
try:
|
|
552
|
+
await self.client.close()
|
|
553
|
+
except Exception as e:
|
|
554
|
+
logger.error(
|
|
555
|
+
"provider_cleanup_failed",
|
|
556
|
+
error=str(e),
|
|
557
|
+
exc_info=e,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# OAuthProviderInfo protocol implementation
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def cli(self) -> CliAuthConfig:
|
|
564
|
+
"""Get CLI authentication configuration for this provider."""
|
|
565
|
+
return CliAuthConfig(
|
|
566
|
+
preferred_flow=FlowType.device,
|
|
567
|
+
callback_port=8080,
|
|
568
|
+
callback_path="/callback",
|
|
569
|
+
supports_manual_code=False,
|
|
570
|
+
supports_device_flow=True,
|
|
571
|
+
fixed_redirect_uri=None,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
def get_provider_info(self) -> OAuthProviderInfo:
|
|
575
|
+
"""Get provider information for registry."""
|
|
576
|
+
return OAuthProviderInfo(
|
|
577
|
+
name=self.provider_name,
|
|
578
|
+
display_name=self.provider_display_name,
|
|
579
|
+
description="GitHub Copilot OAuth authentication",
|
|
580
|
+
supports_pkce=self.supports_pkce,
|
|
581
|
+
scopes=["read:user", "copilot"],
|
|
582
|
+
is_available=True,
|
|
583
|
+
plugin_name="copilot",
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
async def exchange_manual_code(self, code: str) -> Any:
|
|
587
|
+
"""Exchange manual authorization code for tokens.
|
|
588
|
+
|
|
589
|
+
Note: Copilot primarily uses device code flow, but this method
|
|
590
|
+
is provided for completeness.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
code: Authorization code from manual entry
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Copilot credentials object
|
|
597
|
+
"""
|
|
598
|
+
# Copilot doesn't typically support manual code entry as it uses device flow
|
|
599
|
+
# This is a placeholder implementation
|
|
600
|
+
raise NotImplementedError(
|
|
601
|
+
"Copilot uses device code flow. Manual code entry is not supported."
|
|
602
|
+
)
|