ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""GitHub CLI detection service for Copilot plugin."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import shutil
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
|
|
8
|
+
from ccproxy.config.settings import Settings
|
|
9
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
10
|
+
|
|
11
|
+
from .models import CopilotCacheData, CopilotCliInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ccproxy.services.cli_detection import CLIDetectionService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = get_plugin_logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CopilotDetectionService:
|
|
22
|
+
"""GitHub CLI detection and capability discovery service."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, settings: Settings, cli_service: "CLIDetectionService"):
|
|
25
|
+
"""Initialize detection service.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
settings: Application settings
|
|
29
|
+
cli_service: Core CLI detection service
|
|
30
|
+
"""
|
|
31
|
+
self.settings = settings
|
|
32
|
+
self._cli_service = cli_service
|
|
33
|
+
self._cache: CopilotCacheData | None = None
|
|
34
|
+
self._cache_ttl = timedelta(minutes=5) # Cache for 5 minutes
|
|
35
|
+
|
|
36
|
+
async def initialize_detection(self) -> CopilotCacheData:
|
|
37
|
+
"""Initialize GitHub CLI detection and cache results.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Cached detection data
|
|
41
|
+
"""
|
|
42
|
+
if self._cache and not self._is_cache_expired():
|
|
43
|
+
logger.debug(
|
|
44
|
+
"using_cached_detection_data",
|
|
45
|
+
cache_age=(datetime.now() - self._cache.last_check).total_seconds(),
|
|
46
|
+
)
|
|
47
|
+
return self._cache
|
|
48
|
+
|
|
49
|
+
logger.debug("initializing_github_cli_detection")
|
|
50
|
+
|
|
51
|
+
# Check if GitHub CLI is available
|
|
52
|
+
cli_path = self.get_cli_path()
|
|
53
|
+
cli_available = cli_path is not None
|
|
54
|
+
|
|
55
|
+
cli_version = None
|
|
56
|
+
auth_status = None
|
|
57
|
+
username = None
|
|
58
|
+
|
|
59
|
+
if cli_available and cli_path:
|
|
60
|
+
try:
|
|
61
|
+
# Get CLI version
|
|
62
|
+
version_result = await asyncio.create_subprocess_exec(
|
|
63
|
+
*cli_path,
|
|
64
|
+
"--version",
|
|
65
|
+
stdout=asyncio.subprocess.PIPE,
|
|
66
|
+
stderr=asyncio.subprocess.PIPE,
|
|
67
|
+
)
|
|
68
|
+
stdout, stderr = await version_result.communicate()
|
|
69
|
+
|
|
70
|
+
if version_result.returncode == 0:
|
|
71
|
+
version_output = stdout.decode().strip()
|
|
72
|
+
# Parse version from "gh version 2.x.x" format
|
|
73
|
+
for line in version_output.split("\n"):
|
|
74
|
+
if line.startswith("gh version"):
|
|
75
|
+
cli_version = (
|
|
76
|
+
line.split()[2] if len(line.split()) >= 3 else None
|
|
77
|
+
)
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
# Check authentication status
|
|
81
|
+
auth_result = await asyncio.create_subprocess_exec(
|
|
82
|
+
*cli_path,
|
|
83
|
+
"auth",
|
|
84
|
+
"status",
|
|
85
|
+
stdout=asyncio.subprocess.PIPE,
|
|
86
|
+
stderr=asyncio.subprocess.PIPE,
|
|
87
|
+
)
|
|
88
|
+
stdout, stderr = await auth_result.communicate()
|
|
89
|
+
|
|
90
|
+
if auth_result.returncode == 0:
|
|
91
|
+
auth_status = "authenticated"
|
|
92
|
+
auth_output = (
|
|
93
|
+
stderr.decode() + stdout.decode()
|
|
94
|
+
) # gh auth status uses stderr
|
|
95
|
+
|
|
96
|
+
# Extract username from output
|
|
97
|
+
for line in auth_output.split("\n"):
|
|
98
|
+
if "Logged in to github.com as" in line:
|
|
99
|
+
parts = line.split()
|
|
100
|
+
if len(parts) >= 6:
|
|
101
|
+
username = parts[5].strip()
|
|
102
|
+
break
|
|
103
|
+
else:
|
|
104
|
+
auth_status = "not_authenticated"
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.warning(
|
|
108
|
+
"github_cli_check_failed",
|
|
109
|
+
error=str(e),
|
|
110
|
+
exc_info=e,
|
|
111
|
+
)
|
|
112
|
+
auth_status = "check_failed"
|
|
113
|
+
|
|
114
|
+
# Update cache
|
|
115
|
+
self._cache = CopilotCacheData(
|
|
116
|
+
cli_available=cli_available,
|
|
117
|
+
cli_version=cli_version,
|
|
118
|
+
auth_status=auth_status,
|
|
119
|
+
username=username,
|
|
120
|
+
last_check=datetime.now(),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
logger.debug(
|
|
124
|
+
"github_cli_detection_completed",
|
|
125
|
+
cli_available=cli_available,
|
|
126
|
+
cli_version=cli_version,
|
|
127
|
+
auth_status=auth_status,
|
|
128
|
+
username=username,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return self._cache
|
|
132
|
+
|
|
133
|
+
def get_cli_path(self) -> list[str] | None:
|
|
134
|
+
"""Get GitHub CLI command path.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
CLI command as list of strings, or None if not available
|
|
138
|
+
"""
|
|
139
|
+
# Try to find GitHub CLI
|
|
140
|
+
cli_binary = shutil.which("gh")
|
|
141
|
+
if cli_binary:
|
|
142
|
+
return [cli_binary]
|
|
143
|
+
|
|
144
|
+
logger.debug("github_cli_not_found")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def get_cli_health_info(self) -> CopilotCliInfo:
|
|
148
|
+
"""Get GitHub CLI health information.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
CLI health information
|
|
152
|
+
"""
|
|
153
|
+
if not self._cache:
|
|
154
|
+
return CopilotCliInfo(
|
|
155
|
+
available=False,
|
|
156
|
+
version=None,
|
|
157
|
+
authenticated=False,
|
|
158
|
+
username=None,
|
|
159
|
+
error="Detection not initialized - call initialize_detection() first",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return CopilotCliInfo(
|
|
163
|
+
available=self._cache.cli_available,
|
|
164
|
+
version=self._cache.cli_version,
|
|
165
|
+
authenticated=self._cache.auth_status == "authenticated",
|
|
166
|
+
username=self._cache.username,
|
|
167
|
+
error=None if self._cache.cli_available else "GitHub CLI not found in PATH",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _is_cache_expired(self) -> bool:
|
|
171
|
+
"""Check if detection cache has expired.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if cache is expired
|
|
175
|
+
"""
|
|
176
|
+
if not self._cache:
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
return datetime.now() - self._cache.last_check > self._cache_ttl
|
|
180
|
+
|
|
181
|
+
async def refresh_cache(self) -> CopilotCacheData:
|
|
182
|
+
"""Force refresh of detection cache.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Fresh detection data
|
|
186
|
+
"""
|
|
187
|
+
logger.debug("forcing_detection_cache_refresh")
|
|
188
|
+
self._cache = None
|
|
189
|
+
return await self.initialize_detection()
|
|
190
|
+
|
|
191
|
+
def get_recommended_headers(self) -> dict[str, str]:
|
|
192
|
+
"""Get recommended headers for Copilot API requests.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dictionary of headers
|
|
196
|
+
"""
|
|
197
|
+
headers = {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
200
|
+
"Editor-Version": "vscode/1.85.0",
|
|
201
|
+
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
|
202
|
+
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
203
|
+
"X-GitHub-Api-Version": "2025-04-01",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Add CLI version if available
|
|
207
|
+
if self._cache and self._cache.cli_version:
|
|
208
|
+
headers["X-GitHub-CLI-Version"] = self._cache.cli_version
|
|
209
|
+
|
|
210
|
+
return headers
|
|
211
|
+
|
|
212
|
+
async def validate_environment(self) -> dict[str, Any]:
|
|
213
|
+
"""Validate the environment for Copilot usage.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Validation results with status and details
|
|
217
|
+
"""
|
|
218
|
+
await self.initialize_detection()
|
|
219
|
+
|
|
220
|
+
validation = {
|
|
221
|
+
"status": "healthy",
|
|
222
|
+
"details": {
|
|
223
|
+
"github_cli": {
|
|
224
|
+
"available": self._cache.cli_available if self._cache else False,
|
|
225
|
+
"version": self._cache.cli_version if self._cache else None,
|
|
226
|
+
"authenticated": (
|
|
227
|
+
self._cache.auth_status == "authenticated"
|
|
228
|
+
if self._cache
|
|
229
|
+
else False
|
|
230
|
+
),
|
|
231
|
+
"username": self._cache.username if self._cache else None,
|
|
232
|
+
},
|
|
233
|
+
"last_check": self._cache.last_check.isoformat()
|
|
234
|
+
if self._cache
|
|
235
|
+
else None,
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Determine overall health
|
|
240
|
+
issues: list[str] = []
|
|
241
|
+
details = cast(dict[str, Any], validation["details"])
|
|
242
|
+
github_cli = cast(dict[str, Any], details["github_cli"])
|
|
243
|
+
|
|
244
|
+
if not github_cli["available"]:
|
|
245
|
+
issues.append("GitHub CLI not available")
|
|
246
|
+
if not github_cli["authenticated"]:
|
|
247
|
+
issues.append("GitHub CLI not authenticated")
|
|
248
|
+
if not details["copilot_access"]:
|
|
249
|
+
issues.append("No Copilot access detected")
|
|
250
|
+
|
|
251
|
+
if issues:
|
|
252
|
+
validation["status"] = "unhealthy"
|
|
253
|
+
validation["issues"] = issues
|
|
254
|
+
|
|
255
|
+
return validation
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Copilot token manager implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from time import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ccproxy.auth.managers.base import BaseTokenManager
|
|
12
|
+
from ccproxy.auth.managers.token_snapshot import TokenSnapshot
|
|
13
|
+
from ccproxy.auth.oauth.protocol import StandardProfileFields
|
|
14
|
+
from ccproxy.auth.storage.base import TokenStorage
|
|
15
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
16
|
+
|
|
17
|
+
from .config import CopilotOAuthConfig
|
|
18
|
+
from .oauth.client import CopilotOAuthClient
|
|
19
|
+
from .oauth.models import CopilotCredentials
|
|
20
|
+
from .oauth.storage import CopilotOAuthStorage
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = get_plugin_logger()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CopilotTokenManager(BaseTokenManager[CopilotCredentials]):
|
|
27
|
+
"""Manager for GitHub Copilot credential lifecycle."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
storage: TokenStorage[CopilotCredentials] | None = None,
|
|
32
|
+
*,
|
|
33
|
+
config: CopilotOAuthConfig | None = None,
|
|
34
|
+
http_client: httpx.AsyncClient | None = None,
|
|
35
|
+
hook_manager: Any | None = None,
|
|
36
|
+
detection_service: Any | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
storage = storage or CopilotOAuthStorage()
|
|
39
|
+
super().__init__(storage)
|
|
40
|
+
self.config = config or CopilotOAuthConfig()
|
|
41
|
+
self._client = CopilotOAuthClient(
|
|
42
|
+
self.config,
|
|
43
|
+
storage
|
|
44
|
+
if isinstance(storage, CopilotOAuthStorage)
|
|
45
|
+
else CopilotOAuthStorage(),
|
|
46
|
+
http_client=http_client,
|
|
47
|
+
hook_manager=hook_manager,
|
|
48
|
+
detection_service=detection_service,
|
|
49
|
+
)
|
|
50
|
+
self._profile_cache: StandardProfileFields | None = None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
async def create(
|
|
54
|
+
cls,
|
|
55
|
+
storage: TokenStorage[CopilotCredentials] | None = None,
|
|
56
|
+
*,
|
|
57
|
+
config: CopilotOAuthConfig | None = None,
|
|
58
|
+
http_client: httpx.AsyncClient | None = None,
|
|
59
|
+
hook_manager: Any | None = None,
|
|
60
|
+
detection_service: Any | None = None,
|
|
61
|
+
) -> CopilotTokenManager:
|
|
62
|
+
"""Async factory for parity with other managers."""
|
|
63
|
+
return cls(
|
|
64
|
+
storage=storage,
|
|
65
|
+
config=config,
|
|
66
|
+
http_client=http_client,
|
|
67
|
+
hook_manager=hook_manager,
|
|
68
|
+
detection_service=detection_service,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _build_token_snapshot(self, credentials: CopilotCredentials) -> TokenSnapshot:
|
|
72
|
+
"""Construct a token snapshot for Copilot credentials."""
|
|
73
|
+
access_token: str | None = None
|
|
74
|
+
copilot_token = credentials.copilot_token
|
|
75
|
+
if copilot_token and copilot_token.token:
|
|
76
|
+
access_token = copilot_token.token.get_secret_value()
|
|
77
|
+
|
|
78
|
+
refresh_token: str | None = None
|
|
79
|
+
oauth_token = credentials.oauth_token
|
|
80
|
+
if oauth_token.refresh_token:
|
|
81
|
+
refresh_token = oauth_token.refresh_token.get_secret_value()
|
|
82
|
+
|
|
83
|
+
expires_at = None
|
|
84
|
+
if copilot_token and copilot_token.expires_at:
|
|
85
|
+
expires_at = copilot_token.expires_at
|
|
86
|
+
else:
|
|
87
|
+
if oauth_token.expires_in and oauth_token.created_at:
|
|
88
|
+
expires_at = oauth_token.expires_at_datetime
|
|
89
|
+
|
|
90
|
+
scope_value = oauth_token.scope or ""
|
|
91
|
+
scopes = tuple(
|
|
92
|
+
scope
|
|
93
|
+
for scope in (item.strip() for item in scope_value.split(" "))
|
|
94
|
+
if scope
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
extras = {
|
|
98
|
+
"account_type": credentials.account_type,
|
|
99
|
+
"has_copilot_token": bool(credentials.copilot_token),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.debug(
|
|
103
|
+
"copilot_token_snapshot",
|
|
104
|
+
scopes=scopes,
|
|
105
|
+
expires_at=expires_at,
|
|
106
|
+
credentials=credentials,
|
|
107
|
+
access_token=access_token,
|
|
108
|
+
refresh_token=refresh_token,
|
|
109
|
+
)
|
|
110
|
+
return TokenSnapshot(
|
|
111
|
+
provider="copilot",
|
|
112
|
+
access_token=access_token,
|
|
113
|
+
refresh_token=refresh_token,
|
|
114
|
+
expires_at=expires_at,
|
|
115
|
+
scopes=scopes,
|
|
116
|
+
extras=extras,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# ==================================================================
|
|
120
|
+
# BaseTokenManager protocol implementations
|
|
121
|
+
# ==================================================================
|
|
122
|
+
|
|
123
|
+
async def refresh_token(self) -> CopilotCredentials | None:
|
|
124
|
+
credentials = await self.load_credentials()
|
|
125
|
+
if not credentials:
|
|
126
|
+
logger.error("copilot_refresh_no_credentials", category="auth")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
refreshed = await self._client.refresh_copilot_token(credentials)
|
|
131
|
+
# Client already persisted credentials; refresh in-memory caches.
|
|
132
|
+
self._credentials_cache = refreshed
|
|
133
|
+
self._credentials_loaded_at = time()
|
|
134
|
+
self._auth_cache.clear()
|
|
135
|
+
self._profile_cache = None
|
|
136
|
+
return refreshed
|
|
137
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
138
|
+
logger.error(
|
|
139
|
+
"copilot_refresh_failed",
|
|
140
|
+
error=str(exc),
|
|
141
|
+
exc_info=exc,
|
|
142
|
+
category="auth",
|
|
143
|
+
)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def is_expired(self, credentials: CopilotCredentials) -> bool:
|
|
147
|
+
token = credentials.copilot_token
|
|
148
|
+
if not token:
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
now = datetime.now(UTC)
|
|
152
|
+
if token.expires_at and now >= token.expires_at:
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
refresh_deadline = self._compute_refresh_deadline(credentials)
|
|
156
|
+
if refresh_deadline and now >= refresh_deadline:
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
return credentials.oauth_token.is_expired
|
|
160
|
+
|
|
161
|
+
def get_account_id(self, credentials: CopilotCredentials) -> str | None:
|
|
162
|
+
# GitHub account information is part of profile, not raw credentials.
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def get_expiration_time(self, credentials: CopilotCredentials) -> datetime | None:
|
|
166
|
+
candidates: list[datetime] = []
|
|
167
|
+
|
|
168
|
+
token = credentials.copilot_token
|
|
169
|
+
if token:
|
|
170
|
+
if token.expires_at:
|
|
171
|
+
candidates.append(token.expires_at)
|
|
172
|
+
|
|
173
|
+
refresh_deadline = self._compute_refresh_deadline(credentials)
|
|
174
|
+
if refresh_deadline:
|
|
175
|
+
candidates.append(refresh_deadline)
|
|
176
|
+
|
|
177
|
+
if credentials.oauth_token.expires_in and credentials.oauth_token.created_at:
|
|
178
|
+
candidates.append(credentials.oauth_token.expires_at_datetime)
|
|
179
|
+
|
|
180
|
+
if not candidates:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
return min(candidates)
|
|
184
|
+
|
|
185
|
+
# ==================================================================
|
|
186
|
+
# Token access helpers used by adapters/routes
|
|
187
|
+
# ==================================================================
|
|
188
|
+
|
|
189
|
+
async def ensure_copilot_token(self) -> str:
|
|
190
|
+
credentials = await self.load_credentials()
|
|
191
|
+
if not credentials:
|
|
192
|
+
raise ValueError("No Copilot credentials available")
|
|
193
|
+
|
|
194
|
+
if credentials.oauth_token.is_expired:
|
|
195
|
+
raise ValueError("OAuth token expired; re-authentication required")
|
|
196
|
+
|
|
197
|
+
if not credentials.copilot_token or credentials.copilot_token.is_expired:
|
|
198
|
+
logger.debug("copilot_token_refresh_needed", category="auth")
|
|
199
|
+
credentials = await self._client.refresh_copilot_token(credentials)
|
|
200
|
+
self._credentials_cache = credentials
|
|
201
|
+
self._credentials_loaded_at = time()
|
|
202
|
+
self._auth_cache.clear()
|
|
203
|
+
self._profile_cache = None
|
|
204
|
+
|
|
205
|
+
token = credentials.copilot_token
|
|
206
|
+
if not token:
|
|
207
|
+
raise ValueError("Unable to obtain Copilot service token")
|
|
208
|
+
return token.token.get_secret_value()
|
|
209
|
+
|
|
210
|
+
async def ensure_oauth_token(self) -> str:
|
|
211
|
+
credentials = await self.load_credentials()
|
|
212
|
+
if not credentials:
|
|
213
|
+
raise ValueError("No Copilot credentials available")
|
|
214
|
+
if credentials.oauth_token.is_expired:
|
|
215
|
+
raise ValueError("OAuth token expired; re-authentication required")
|
|
216
|
+
return credentials.oauth_token.access_token.get_secret_value()
|
|
217
|
+
|
|
218
|
+
async def get_access_token(self) -> str | None:
|
|
219
|
+
try:
|
|
220
|
+
return await self.ensure_copilot_token()
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
logger.error(
|
|
223
|
+
"copilot_access_token_failed",
|
|
224
|
+
error=str(exc),
|
|
225
|
+
category="auth",
|
|
226
|
+
)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
async def get_access_token_with_refresh(self) -> str | None:
|
|
230
|
+
return await self.get_access_token()
|
|
231
|
+
|
|
232
|
+
async def get_profile(self) -> StandardProfileFields | None:
|
|
233
|
+
if self._profile_cache:
|
|
234
|
+
return self._profile_cache
|
|
235
|
+
credentials = await self.load_credentials()
|
|
236
|
+
if not credentials:
|
|
237
|
+
return None
|
|
238
|
+
try:
|
|
239
|
+
profile = await self._client.get_standard_profile(credentials.oauth_token)
|
|
240
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
241
|
+
logger.debug("copilot_profile_fetch_failed", error=str(exc))
|
|
242
|
+
return None
|
|
243
|
+
self._profile_cache = profile
|
|
244
|
+
return profile
|
|
245
|
+
|
|
246
|
+
async def get_profile_quick(self) -> StandardProfileFields | None:
|
|
247
|
+
return await self.get_profile()
|
|
248
|
+
|
|
249
|
+
async def aclose(self) -> None:
|
|
250
|
+
await self._client.close()
|
|
251
|
+
|
|
252
|
+
def _compute_refresh_deadline(
|
|
253
|
+
self, credentials: CopilotCredentials
|
|
254
|
+
) -> datetime | None:
|
|
255
|
+
token = credentials.copilot_token
|
|
256
|
+
if not token or not token.refresh_in:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
updated_at = int(credentials.updated_at)
|
|
261
|
+
except (TypeError, ValueError):
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
refresh_in = int(token.refresh_in)
|
|
266
|
+
except (TypeError, ValueError):
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
if refresh_in <= 0:
|
|
270
|
+
return datetime.now(UTC)
|
|
271
|
+
|
|
272
|
+
return datetime.fromtimestamp(updated_at + refresh_in, tz=UTC)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
__all__ = ["CopilotTokenManager"]
|