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
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
"""Credentials manager for coordinating storage and OAuth operations."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
from datetime import UTC, datetime, timedelta
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
import httpx
|
|
10
|
-
from structlog import get_logger
|
|
11
|
-
|
|
12
|
-
from ccproxy.auth.exceptions import (
|
|
13
|
-
CredentialsExpiredError,
|
|
14
|
-
CredentialsNotFoundError,
|
|
15
|
-
)
|
|
16
|
-
from ccproxy.auth.models import (
|
|
17
|
-
ClaudeCredentials,
|
|
18
|
-
OAuthToken,
|
|
19
|
-
UserProfile,
|
|
20
|
-
ValidationResult,
|
|
21
|
-
)
|
|
22
|
-
from ccproxy.auth.storage import JsonFileTokenStorage as JsonFileStorage
|
|
23
|
-
from ccproxy.auth.storage import TokenStorage as CredentialsStorageBackend
|
|
24
|
-
from ccproxy.config.auth import AuthSettings
|
|
25
|
-
from ccproxy.services.credentials.oauth_client import OAuthClient
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
logger = get_logger(__name__)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class CredentialsManager:
|
|
32
|
-
"""Manager for Claude credentials with storage and OAuth support."""
|
|
33
|
-
|
|
34
|
-
# ==================== Initialization ====================
|
|
35
|
-
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
config: AuthSettings | None = None,
|
|
39
|
-
storage: CredentialsStorageBackend | None = None,
|
|
40
|
-
oauth_client: OAuthClient | None = None,
|
|
41
|
-
http_client: httpx.AsyncClient | None = None,
|
|
42
|
-
):
|
|
43
|
-
"""Initialize credentials manager.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
config: Credentials configuration (uses defaults if not provided)
|
|
47
|
-
storage: Storage backend (uses JSON file storage if not provided)
|
|
48
|
-
oauth_client: OAuth client (creates one if not provided)
|
|
49
|
-
http_client: HTTP client for OAuth operations
|
|
50
|
-
"""
|
|
51
|
-
self.config = config or AuthSettings()
|
|
52
|
-
self._storage = storage
|
|
53
|
-
self._oauth_client = oauth_client
|
|
54
|
-
self._http_client = http_client
|
|
55
|
-
self._owns_http_client = http_client is None
|
|
56
|
-
self._refresh_lock = asyncio.Lock()
|
|
57
|
-
|
|
58
|
-
# Initialize OAuth client if not provided
|
|
59
|
-
if self._oauth_client is None:
|
|
60
|
-
self._oauth_client = OAuthClient(
|
|
61
|
-
config=self.config.oauth,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
async def __aenter__(self) -> "CredentialsManager":
|
|
65
|
-
"""Async context manager entry."""
|
|
66
|
-
if self._http_client is None:
|
|
67
|
-
self._http_client = httpx.AsyncClient()
|
|
68
|
-
return self
|
|
69
|
-
|
|
70
|
-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
71
|
-
"""Async context manager exit."""
|
|
72
|
-
if self._owns_http_client and self._http_client:
|
|
73
|
-
await self._http_client.aclose()
|
|
74
|
-
|
|
75
|
-
# ==================== Storage Operations ====================
|
|
76
|
-
|
|
77
|
-
@property
|
|
78
|
-
def storage(self) -> CredentialsStorageBackend:
|
|
79
|
-
"""Get the storage backend, creating default if needed."""
|
|
80
|
-
if self._storage is None:
|
|
81
|
-
# Find first existing credentials file or use first path
|
|
82
|
-
existing_path = self._find_existing_path()
|
|
83
|
-
if existing_path:
|
|
84
|
-
self._storage = JsonFileStorage(existing_path)
|
|
85
|
-
else:
|
|
86
|
-
# Use first path as default
|
|
87
|
-
self._storage = JsonFileStorage(
|
|
88
|
-
Path(self.config.storage.storage_paths[0]).expanduser()
|
|
89
|
-
)
|
|
90
|
-
return self._storage
|
|
91
|
-
|
|
92
|
-
async def find_credentials_file(self) -> Path | None:
|
|
93
|
-
"""Find existing credentials file in configured paths.
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
Path to credentials file if found, None otherwise
|
|
97
|
-
"""
|
|
98
|
-
for path_str in self.config.storage.storage_paths:
|
|
99
|
-
path = Path(path_str).expanduser()
|
|
100
|
-
logger.debug("checking_credentials_path", path=str(path))
|
|
101
|
-
if path.exists() and path.is_file():
|
|
102
|
-
logger.info("credentials_file_found", path=str(path))
|
|
103
|
-
return path
|
|
104
|
-
else:
|
|
105
|
-
logger.debug("credentials_path_not_found", path=str(path))
|
|
106
|
-
|
|
107
|
-
logger.warning(
|
|
108
|
-
"no_credentials_files_found",
|
|
109
|
-
searched_paths=self.config.storage.storage_paths,
|
|
110
|
-
)
|
|
111
|
-
return None
|
|
112
|
-
|
|
113
|
-
async def load(self) -> ClaudeCredentials | None:
|
|
114
|
-
"""Load credentials from storage.
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
Credentials if found and valid, None otherwise
|
|
118
|
-
"""
|
|
119
|
-
try:
|
|
120
|
-
return await self.storage.load()
|
|
121
|
-
except Exception as e:
|
|
122
|
-
logger.error("credentials_load_failed", error=str(e))
|
|
123
|
-
return None
|
|
124
|
-
|
|
125
|
-
async def save(self, credentials: ClaudeCredentials) -> bool:
|
|
126
|
-
"""Save credentials to storage.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
credentials: Credentials to save
|
|
130
|
-
|
|
131
|
-
Returns:
|
|
132
|
-
True if saved successfully, False otherwise
|
|
133
|
-
"""
|
|
134
|
-
try:
|
|
135
|
-
return await self.storage.save(credentials)
|
|
136
|
-
except Exception as e:
|
|
137
|
-
logger.error("credentials_save_failed", error=str(e))
|
|
138
|
-
return False
|
|
139
|
-
|
|
140
|
-
# ==================== OAuth Operations ====================
|
|
141
|
-
|
|
142
|
-
async def login(self) -> ClaudeCredentials:
|
|
143
|
-
"""Perform OAuth login and save credentials.
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
New credentials from login
|
|
147
|
-
|
|
148
|
-
Raises:
|
|
149
|
-
OAuthLoginError: If login fails
|
|
150
|
-
"""
|
|
151
|
-
if self._oauth_client is None:
|
|
152
|
-
raise RuntimeError("OAuth client not initialized")
|
|
153
|
-
credentials = await self._oauth_client.login()
|
|
154
|
-
|
|
155
|
-
# Fetch and save user profile after successful login
|
|
156
|
-
try:
|
|
157
|
-
profile = await self._oauth_client.fetch_user_profile(
|
|
158
|
-
credentials.claude_ai_oauth.access_token
|
|
159
|
-
)
|
|
160
|
-
if profile:
|
|
161
|
-
# Save profile data
|
|
162
|
-
await self._save_account_profile(profile)
|
|
163
|
-
|
|
164
|
-
# Update subscription type based on profile
|
|
165
|
-
determined_subscription = self._determine_subscription_type(profile)
|
|
166
|
-
credentials.claude_ai_oauth.subscription_type = determined_subscription
|
|
167
|
-
|
|
168
|
-
logger.debug(
|
|
169
|
-
"subscription_type_set", subscription_type=determined_subscription
|
|
170
|
-
)
|
|
171
|
-
else:
|
|
172
|
-
logger.debug(
|
|
173
|
-
"profile_fetch_skipped", context="login", reason="no_profile_data"
|
|
174
|
-
)
|
|
175
|
-
except Exception as e:
|
|
176
|
-
logger.warning("profile_fetch_failed", context="login", error=str(e))
|
|
177
|
-
# Continue with login even if profile fetch fails
|
|
178
|
-
|
|
179
|
-
await self.save(credentials)
|
|
180
|
-
return credentials
|
|
181
|
-
|
|
182
|
-
async def get_valid_credentials(self) -> ClaudeCredentials:
|
|
183
|
-
"""Get valid credentials, refreshing if necessary.
|
|
184
|
-
|
|
185
|
-
Returns:
|
|
186
|
-
Valid credentials
|
|
187
|
-
|
|
188
|
-
Raises:
|
|
189
|
-
CredentialsNotFoundError: If no credentials found
|
|
190
|
-
CredentialsExpiredError: If credentials expired and refresh fails
|
|
191
|
-
"""
|
|
192
|
-
credentials = await self.load()
|
|
193
|
-
if not credentials:
|
|
194
|
-
raise CredentialsNotFoundError("No credentials found. Please login first.")
|
|
195
|
-
|
|
196
|
-
# Check if token needs refresh
|
|
197
|
-
oauth_token = credentials.claude_ai_oauth
|
|
198
|
-
should_refresh = self._should_refresh_token(oauth_token)
|
|
199
|
-
|
|
200
|
-
if should_refresh:
|
|
201
|
-
async with self._refresh_lock:
|
|
202
|
-
# Re-check if refresh is still needed after acquiring lock
|
|
203
|
-
# Another request might have already refreshed the token
|
|
204
|
-
credentials = await self.load()
|
|
205
|
-
if not credentials:
|
|
206
|
-
raise CredentialsNotFoundError(
|
|
207
|
-
"No credentials found. Please login first."
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
oauth_token = credentials.claude_ai_oauth
|
|
211
|
-
should_refresh = self._should_refresh_token(oauth_token)
|
|
212
|
-
|
|
213
|
-
if should_refresh:
|
|
214
|
-
logger.info(
|
|
215
|
-
"token_refresh_start", reason="expired_or_expiring_soon"
|
|
216
|
-
)
|
|
217
|
-
try:
|
|
218
|
-
credentials = await self._refresh_token_with_profile(
|
|
219
|
-
credentials
|
|
220
|
-
)
|
|
221
|
-
except Exception as e:
|
|
222
|
-
logger.error(
|
|
223
|
-
"token_refresh_failed", error=str(e), exc_info=True
|
|
224
|
-
)
|
|
225
|
-
if oauth_token.is_expired:
|
|
226
|
-
raise CredentialsExpiredError(
|
|
227
|
-
"Token expired and refresh failed. Please login again."
|
|
228
|
-
) from e
|
|
229
|
-
# If not expired yet but refresh failed, return existing token
|
|
230
|
-
logger.warning(
|
|
231
|
-
"token_refresh_fallback",
|
|
232
|
-
reason="refresh_failed_but_token_not_expired",
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
return credentials
|
|
236
|
-
|
|
237
|
-
async def get_access_token(self) -> str:
|
|
238
|
-
"""Get valid access token, refreshing if necessary.
|
|
239
|
-
|
|
240
|
-
Returns:
|
|
241
|
-
Access token string
|
|
242
|
-
|
|
243
|
-
Raises:
|
|
244
|
-
CredentialsNotFoundError: If no credentials found
|
|
245
|
-
CredentialsExpiredError: If credentials expired and refresh fails
|
|
246
|
-
"""
|
|
247
|
-
credentials = await self.get_valid_credentials()
|
|
248
|
-
return credentials.claude_ai_oauth.access_token
|
|
249
|
-
|
|
250
|
-
async def refresh_token(self) -> ClaudeCredentials:
|
|
251
|
-
"""Refresh the access token without checking expiration.
|
|
252
|
-
|
|
253
|
-
This method directly refreshes the token regardless of whether it's expired.
|
|
254
|
-
Useful for force-refreshing tokens or testing.
|
|
255
|
-
|
|
256
|
-
Returns:
|
|
257
|
-
Updated credentials with new token
|
|
258
|
-
|
|
259
|
-
Raises:
|
|
260
|
-
CredentialsNotFoundError: If no credentials found
|
|
261
|
-
RuntimeError: If OAuth client not initialized
|
|
262
|
-
ValueError: If no refresh token available
|
|
263
|
-
Exception: If token refresh fails
|
|
264
|
-
"""
|
|
265
|
-
credentials = await self.load()
|
|
266
|
-
if not credentials:
|
|
267
|
-
raise CredentialsNotFoundError("No credentials found. Please login first.")
|
|
268
|
-
|
|
269
|
-
logger.info("token_refresh_start", reason="forced")
|
|
270
|
-
return await self._refresh_token_with_profile(credentials)
|
|
271
|
-
|
|
272
|
-
async def fetch_user_profile(self) -> UserProfile | None:
|
|
273
|
-
"""Fetch user profile information.
|
|
274
|
-
|
|
275
|
-
Returns:
|
|
276
|
-
UserProfile if successful, None otherwise
|
|
277
|
-
"""
|
|
278
|
-
try:
|
|
279
|
-
credentials = await self.get_valid_credentials()
|
|
280
|
-
if self._oauth_client is None:
|
|
281
|
-
raise RuntimeError("OAuth client not initialized")
|
|
282
|
-
profile = await self._oauth_client.fetch_user_profile(
|
|
283
|
-
credentials.claude_ai_oauth.access_token,
|
|
284
|
-
)
|
|
285
|
-
return profile
|
|
286
|
-
except Exception as e:
|
|
287
|
-
logger.error(
|
|
288
|
-
"user_profile_fetch_failed",
|
|
289
|
-
error=str(e),
|
|
290
|
-
exc_info=True,
|
|
291
|
-
)
|
|
292
|
-
return None
|
|
293
|
-
|
|
294
|
-
async def get_account_profile(self) -> UserProfile | None:
|
|
295
|
-
"""Get saved account profile information.
|
|
296
|
-
|
|
297
|
-
Returns:
|
|
298
|
-
UserProfile if available, None otherwise
|
|
299
|
-
"""
|
|
300
|
-
return await self._load_account_profile()
|
|
301
|
-
|
|
302
|
-
# ==================== Validation and Management ====================
|
|
303
|
-
|
|
304
|
-
async def validate(self) -> ValidationResult:
|
|
305
|
-
"""Validate current credentials.
|
|
306
|
-
|
|
307
|
-
Returns:
|
|
308
|
-
ValidationResult with credentials status and details
|
|
309
|
-
"""
|
|
310
|
-
credentials = await self.load()
|
|
311
|
-
if not credentials:
|
|
312
|
-
raise CredentialsNotFoundError()
|
|
313
|
-
|
|
314
|
-
return ValidationResult(
|
|
315
|
-
valid=True,
|
|
316
|
-
expired=credentials.claude_ai_oauth.is_expired,
|
|
317
|
-
credentials=credentials,
|
|
318
|
-
path=self.storage.get_location(),
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
async def logout(self) -> bool:
|
|
322
|
-
"""Delete stored credentials.
|
|
323
|
-
|
|
324
|
-
Returns:
|
|
325
|
-
True if deleted successfully, False otherwise
|
|
326
|
-
"""
|
|
327
|
-
try:
|
|
328
|
-
# Delete both credentials and account profile
|
|
329
|
-
success = await self.storage.delete()
|
|
330
|
-
await self._delete_account_profile()
|
|
331
|
-
return success
|
|
332
|
-
except Exception as e:
|
|
333
|
-
logger.error("credentials_delete_failed", error=str(e), exc_info=True)
|
|
334
|
-
return False
|
|
335
|
-
|
|
336
|
-
# ==================== Private Helper Methods ====================
|
|
337
|
-
|
|
338
|
-
async def _get_account_profile_path(self) -> Path:
|
|
339
|
-
"""Get the path for account profile storage.
|
|
340
|
-
|
|
341
|
-
Returns:
|
|
342
|
-
Path to account.json file alongside credentials
|
|
343
|
-
"""
|
|
344
|
-
# Use the same directory as credentials file but with account.json name
|
|
345
|
-
credentials_path = self._find_existing_path()
|
|
346
|
-
if credentials_path is None:
|
|
347
|
-
# Use first path as default
|
|
348
|
-
credentials_path = Path(self.config.storage.storage_paths[0]).expanduser()
|
|
349
|
-
|
|
350
|
-
# Replace filename with account.json
|
|
351
|
-
return credentials_path.parent / "account.json"
|
|
352
|
-
|
|
353
|
-
async def _save_account_profile(self, profile: UserProfile) -> bool:
|
|
354
|
-
"""Save account profile to account.json.
|
|
355
|
-
|
|
356
|
-
Args:
|
|
357
|
-
profile: User profile to save
|
|
358
|
-
|
|
359
|
-
Returns:
|
|
360
|
-
True if saved successfully
|
|
361
|
-
"""
|
|
362
|
-
try:
|
|
363
|
-
account_path = await self._get_account_profile_path()
|
|
364
|
-
account_path.parent.mkdir(parents=True, exist_ok=True)
|
|
365
|
-
|
|
366
|
-
# Convert to dict and save as JSON
|
|
367
|
-
profile_data = profile.model_dump()
|
|
368
|
-
|
|
369
|
-
with account_path.open("w", encoding="utf-8") as f:
|
|
370
|
-
json.dump(profile_data, f, indent=2, ensure_ascii=False)
|
|
371
|
-
|
|
372
|
-
logger.debug("account_profile_saved", path=str(account_path))
|
|
373
|
-
return True
|
|
374
|
-
|
|
375
|
-
except Exception as e:
|
|
376
|
-
logger.error("account_profile_save_failed", error=str(e), exc_info=True)
|
|
377
|
-
return False
|
|
378
|
-
|
|
379
|
-
async def _load_account_profile(self) -> UserProfile | None:
|
|
380
|
-
"""Load account profile from account.json.
|
|
381
|
-
|
|
382
|
-
Returns:
|
|
383
|
-
User profile if found, None otherwise
|
|
384
|
-
"""
|
|
385
|
-
try:
|
|
386
|
-
account_path = await self._get_account_profile_path()
|
|
387
|
-
|
|
388
|
-
if not account_path.exists():
|
|
389
|
-
logger.debug("account_profile_not_found", path=str(account_path))
|
|
390
|
-
return None
|
|
391
|
-
|
|
392
|
-
with account_path.open("r", encoding="utf-8") as f:
|
|
393
|
-
profile_data = json.load(f)
|
|
394
|
-
|
|
395
|
-
return UserProfile.model_validate(profile_data)
|
|
396
|
-
|
|
397
|
-
except Exception as e:
|
|
398
|
-
logger.debug("account_profile_load_failed", error=str(e))
|
|
399
|
-
return None
|
|
400
|
-
|
|
401
|
-
async def _delete_account_profile(self) -> bool:
|
|
402
|
-
"""Delete account profile file.
|
|
403
|
-
|
|
404
|
-
Returns:
|
|
405
|
-
True if deleted successfully
|
|
406
|
-
"""
|
|
407
|
-
try:
|
|
408
|
-
account_path = await self._get_account_profile_path()
|
|
409
|
-
if account_path.exists():
|
|
410
|
-
account_path.unlink()
|
|
411
|
-
logger.debug("account_profile_deleted", path=str(account_path))
|
|
412
|
-
return True
|
|
413
|
-
except Exception as e:
|
|
414
|
-
logger.debug("account_profile_delete_failed", error=str(e))
|
|
415
|
-
return False
|
|
416
|
-
|
|
417
|
-
def _determine_subscription_type(self, profile: UserProfile) -> str:
|
|
418
|
-
"""Determine subscription type from profile information.
|
|
419
|
-
|
|
420
|
-
Args:
|
|
421
|
-
profile: User profile with account information
|
|
422
|
-
|
|
423
|
-
Returns:
|
|
424
|
-
Subscription type string
|
|
425
|
-
"""
|
|
426
|
-
if not profile.account:
|
|
427
|
-
return "unknown"
|
|
428
|
-
|
|
429
|
-
# Check account flags first
|
|
430
|
-
if profile.account.has_claude_max:
|
|
431
|
-
return "max"
|
|
432
|
-
elif profile.account.has_claude_pro:
|
|
433
|
-
return "pro"
|
|
434
|
-
|
|
435
|
-
# Fallback to organization type
|
|
436
|
-
if profile.organization and profile.organization.organization_type:
|
|
437
|
-
org_type = profile.organization.organization_type.lower()
|
|
438
|
-
if "max" in org_type:
|
|
439
|
-
return "max"
|
|
440
|
-
elif "pro" in org_type:
|
|
441
|
-
return "pro"
|
|
442
|
-
|
|
443
|
-
return "free"
|
|
444
|
-
|
|
445
|
-
def _find_existing_path(self) -> Path | None:
|
|
446
|
-
"""Find first existing path from configured storage paths.
|
|
447
|
-
|
|
448
|
-
Returns:
|
|
449
|
-
Path if found, None otherwise
|
|
450
|
-
"""
|
|
451
|
-
for path_str in self.config.storage.storage_paths:
|
|
452
|
-
path = Path(path_str).expanduser()
|
|
453
|
-
if path.exists():
|
|
454
|
-
return path
|
|
455
|
-
return None
|
|
456
|
-
|
|
457
|
-
def _should_refresh_token(self, oauth_token: OAuthToken) -> bool:
|
|
458
|
-
"""Check if token should be refreshed based on configuration.
|
|
459
|
-
|
|
460
|
-
Args:
|
|
461
|
-
oauth_token: Token to check
|
|
462
|
-
|
|
463
|
-
Returns:
|
|
464
|
-
True if token should be refreshed
|
|
465
|
-
"""
|
|
466
|
-
if self.config.storage.auto_refresh:
|
|
467
|
-
buffer = timedelta(seconds=self.config.storage.refresh_buffer_seconds)
|
|
468
|
-
return datetime.now(UTC) + buffer >= oauth_token.expires_at_datetime
|
|
469
|
-
else:
|
|
470
|
-
return oauth_token.is_expired
|
|
471
|
-
|
|
472
|
-
async def _refresh_token_with_profile(
|
|
473
|
-
self, credentials: ClaudeCredentials
|
|
474
|
-
) -> ClaudeCredentials:
|
|
475
|
-
"""Refresh token and update profile information.
|
|
476
|
-
|
|
477
|
-
Args:
|
|
478
|
-
credentials: Current credentials with token to refresh
|
|
479
|
-
|
|
480
|
-
Returns:
|
|
481
|
-
Updated credentials with new token and profile info
|
|
482
|
-
|
|
483
|
-
Raises:
|
|
484
|
-
RuntimeError: If OAuth client not initialized
|
|
485
|
-
ValueError: If no refresh token available
|
|
486
|
-
Exception: If token refresh fails
|
|
487
|
-
"""
|
|
488
|
-
if self._oauth_client is None:
|
|
489
|
-
raise RuntimeError("OAuth client not initialized")
|
|
490
|
-
|
|
491
|
-
oauth_token = credentials.claude_ai_oauth
|
|
492
|
-
|
|
493
|
-
# Refresh the token
|
|
494
|
-
token_response = await self._oauth_client.refresh_access_token(
|
|
495
|
-
oauth_token.refresh_token
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
# Calculate expires_at from expires_in if provided
|
|
499
|
-
expires_at = oauth_token.expires_at # Start with existing value
|
|
500
|
-
if token_response.expires_in:
|
|
501
|
-
expires_at = int(
|
|
502
|
-
(datetime.now(UTC).timestamp() + token_response.expires_in) * 1000
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
# Parse scopes from server response
|
|
506
|
-
new_scopes = oauth_token.scopes # Start with existing scopes
|
|
507
|
-
if token_response.scope:
|
|
508
|
-
new_scopes = token_response.scope.split()
|
|
509
|
-
|
|
510
|
-
# Create new token preserving all server fields when available
|
|
511
|
-
# Ensure we have valid refresh token
|
|
512
|
-
if not token_response.refresh_token and not oauth_token.refresh_token:
|
|
513
|
-
raise ValueError("No refresh token available")
|
|
514
|
-
|
|
515
|
-
# Convert OAuthTokenResponse to OAuthToken format
|
|
516
|
-
new_token = OAuthToken(
|
|
517
|
-
accessToken=token_response.access_token,
|
|
518
|
-
refreshToken=token_response.refresh_token or oauth_token.refresh_token,
|
|
519
|
-
expiresAt=expires_at,
|
|
520
|
-
scopes=new_scopes,
|
|
521
|
-
subscriptionType=token_response.subscription_type
|
|
522
|
-
or oauth_token.subscription_type,
|
|
523
|
-
tokenType=token_response.token_type or oauth_token.token_type,
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
# Update credentials with new token
|
|
527
|
-
credentials.claude_ai_oauth = new_token
|
|
528
|
-
|
|
529
|
-
# Fetch user profile to update subscription type
|
|
530
|
-
try:
|
|
531
|
-
profile = await self._oauth_client.fetch_user_profile(
|
|
532
|
-
new_token.access_token
|
|
533
|
-
)
|
|
534
|
-
if profile:
|
|
535
|
-
# Save profile data
|
|
536
|
-
await self._save_account_profile(profile)
|
|
537
|
-
|
|
538
|
-
# Update subscription type based on profile
|
|
539
|
-
determined_subscription = self._determine_subscription_type(profile)
|
|
540
|
-
new_token.subscription_type = determined_subscription
|
|
541
|
-
credentials.claude_ai_oauth = new_token
|
|
542
|
-
|
|
543
|
-
logger.debug(
|
|
544
|
-
"subscription_type_updated",
|
|
545
|
-
subscription_type=determined_subscription,
|
|
546
|
-
)
|
|
547
|
-
else:
|
|
548
|
-
logger.debug(
|
|
549
|
-
"profile_fetch_skipped", reason="no_profile_data_available"
|
|
550
|
-
)
|
|
551
|
-
except Exception as e:
|
|
552
|
-
logger.warning(
|
|
553
|
-
"profile_fetch_failed", context="token_refresh", error=str(e)
|
|
554
|
-
)
|
|
555
|
-
# Continue with token refresh even if profile fetch fails
|
|
556
|
-
|
|
557
|
-
# Save updated credentials
|
|
558
|
-
await self.save(credentials)
|
|
559
|
-
|
|
560
|
-
logger.info("token_refresh_completed")
|
|
561
|
-
return credentials
|