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,482 @@
|
|
|
1
|
+
"""Claude API token manager implementation for the Claude API plugin."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol, cast
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
from ccproxy.auth.managers.base_enhanced import EnhancedTokenManager
|
|
13
|
+
from ccproxy.auth.managers.token_snapshot import TokenSnapshot
|
|
14
|
+
from ccproxy.auth.storage.base import TokenStorage
|
|
15
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
16
|
+
|
|
17
|
+
from .config import ClaudeOAuthConfig
|
|
18
|
+
from .models import ClaudeCredentials, ClaudeProfileInfo, ClaudeTokenWrapper
|
|
19
|
+
from .storage import ClaudeOAuthStorage, ClaudeProfileStorage
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TokenRefreshProvider(Protocol):
|
|
23
|
+
"""Protocol for token refresh capability."""
|
|
24
|
+
|
|
25
|
+
async def refresh_access_token(self, refresh_token: str) -> ClaudeCredentials:
|
|
26
|
+
"""Refresh access token using refresh token."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = get_plugin_logger()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ClaudeApiTokenManager(EnhancedTokenManager[ClaudeCredentials]):
|
|
34
|
+
"""Manager for Claude API token storage and refresh operations.
|
|
35
|
+
|
|
36
|
+
Uses the Claude-specific storage implementation with enhanced token management.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
storage: TokenStorage[ClaudeCredentials] | None = None,
|
|
42
|
+
http_client: "httpx.AsyncClient | None" = None,
|
|
43
|
+
oauth_provider: TokenRefreshProvider | None = None,
|
|
44
|
+
):
|
|
45
|
+
"""Initialize Claude API token manager.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
storage: Optional custom storage, defaults to standard location
|
|
49
|
+
http_client: Optional HTTP client for API requests
|
|
50
|
+
oauth_provider: Optional OAuth provider for token refresh (protocol injection)
|
|
51
|
+
"""
|
|
52
|
+
if storage is None:
|
|
53
|
+
storage = ClaudeOAuthStorage()
|
|
54
|
+
super().__init__(storage)
|
|
55
|
+
self._profile_cache: ClaudeProfileInfo | None = None
|
|
56
|
+
self.oauth_provider = oauth_provider
|
|
57
|
+
|
|
58
|
+
# Create default HTTP client if not provided; track ownership
|
|
59
|
+
self._owns_client = False
|
|
60
|
+
if http_client is None:
|
|
61
|
+
http_client = httpx.AsyncClient()
|
|
62
|
+
self._owns_client = True
|
|
63
|
+
self.http_client = http_client
|
|
64
|
+
|
|
65
|
+
# ==================== Internal helpers ====================
|
|
66
|
+
|
|
67
|
+
def _derive_subscription_type(self, profile: "ClaudeProfileInfo") -> str:
|
|
68
|
+
"""Derive subscription type string from profile info.
|
|
69
|
+
|
|
70
|
+
Priority: "max" > "pro" > "free".
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
if getattr(profile, "has_claude_max", None):
|
|
74
|
+
return "max"
|
|
75
|
+
if getattr(profile, "has_claude_pro", None):
|
|
76
|
+
return "pro"
|
|
77
|
+
return "free"
|
|
78
|
+
except Exception:
|
|
79
|
+
# Be defensive; default to free if unexpected structure
|
|
80
|
+
return "free"
|
|
81
|
+
|
|
82
|
+
async def _sync_subscription_type_with_profile(
|
|
83
|
+
self,
|
|
84
|
+
profile: "ClaudeProfileInfo",
|
|
85
|
+
credentials: "ClaudeCredentials | None" = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Update stored credentials with subscription type from profile.
|
|
88
|
+
|
|
89
|
+
Avoids unnecessary writes by only saving when the value changes.
|
|
90
|
+
If credentials are not provided, they will be loaded once.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
new_sub = self._derive_subscription_type(profile)
|
|
94
|
+
|
|
95
|
+
# Use provided credentials to avoid an extra read if available
|
|
96
|
+
creds = credentials or await self.load_credentials()
|
|
97
|
+
if not creds or not hasattr(creds, "claude_ai_oauth"):
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
current_sub = creds.claude_ai_oauth.subscription_type
|
|
101
|
+
if current_sub != new_sub:
|
|
102
|
+
creds.claude_ai_oauth.subscription_type = new_sub
|
|
103
|
+
await self.save_credentials(creds)
|
|
104
|
+
logger.info(
|
|
105
|
+
"claude_subscription_type_updated",
|
|
106
|
+
subscription_type=new_sub,
|
|
107
|
+
category="auth",
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
# Non-fatal: syncing subscription type should never break profile flow
|
|
111
|
+
logger.debug(
|
|
112
|
+
"claude_subscription_type_update_failed",
|
|
113
|
+
error=str(e),
|
|
114
|
+
category="auth",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
async def create(
|
|
119
|
+
cls,
|
|
120
|
+
storage: TokenStorage["ClaudeCredentials"] | None = None,
|
|
121
|
+
http_client: "httpx.AsyncClient | None" = None,
|
|
122
|
+
oauth_provider: TokenRefreshProvider | None = None,
|
|
123
|
+
) -> "ClaudeApiTokenManager":
|
|
124
|
+
"""Async factory that constructs the manager and preloads cached profile.
|
|
125
|
+
|
|
126
|
+
This avoids creating event loops in __init__ and keeps initialization non-blocking.
|
|
127
|
+
"""
|
|
128
|
+
manager = cls(
|
|
129
|
+
storage=storage, http_client=http_client, oauth_provider=oauth_provider
|
|
130
|
+
)
|
|
131
|
+
await manager.preload_profile_cache()
|
|
132
|
+
return manager
|
|
133
|
+
|
|
134
|
+
def _build_token_snapshot(self, credentials: ClaudeCredentials) -> TokenSnapshot:
|
|
135
|
+
"""Construct a token snapshot for Claude credentials."""
|
|
136
|
+
wrapper = ClaudeTokenWrapper(credentials=credentials)
|
|
137
|
+
scopes = tuple(wrapper.scopes)
|
|
138
|
+
extras = {
|
|
139
|
+
"subscription_type": wrapper.subscription_type,
|
|
140
|
+
}
|
|
141
|
+
return TokenSnapshot(
|
|
142
|
+
provider="claude-api",
|
|
143
|
+
access_token=str(wrapper.access_token_value),
|
|
144
|
+
refresh_token=wrapper.refresh_token_value,
|
|
145
|
+
expires_at=wrapper.expires_at_datetime,
|
|
146
|
+
scopes=scopes,
|
|
147
|
+
extras=extras,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def preload_profile_cache(self) -> None:
|
|
151
|
+
"""Load profile from storage asynchronously if available."""
|
|
152
|
+
try:
|
|
153
|
+
profile_storage = ClaudeProfileStorage()
|
|
154
|
+
|
|
155
|
+
# Only attempt to read if the file exists
|
|
156
|
+
if profile_storage.file_path.exists():
|
|
157
|
+
profile = await profile_storage.load_profile()
|
|
158
|
+
if profile:
|
|
159
|
+
self._profile_cache = profile
|
|
160
|
+
logger.debug(
|
|
161
|
+
"claude_profile_loaded_from_cache",
|
|
162
|
+
account_id=profile.account_id,
|
|
163
|
+
email=profile.email,
|
|
164
|
+
category="auth",
|
|
165
|
+
)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
# Don't fail if profile can't be loaded
|
|
168
|
+
logger.debug(
|
|
169
|
+
"claude_profile_cache_load_failed",
|
|
170
|
+
error=str(e),
|
|
171
|
+
category="auth",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# ==================== Enhanced Token Management Methods ====================
|
|
175
|
+
|
|
176
|
+
async def get_access_token(self) -> str:
|
|
177
|
+
"""Get access token using enhanced base with automatic refresh."""
|
|
178
|
+
token = await self.get_access_token_with_refresh()
|
|
179
|
+
if not token:
|
|
180
|
+
from ccproxy.auth.exceptions import CredentialsInvalidError
|
|
181
|
+
|
|
182
|
+
raise CredentialsInvalidError("No valid access token available")
|
|
183
|
+
return token
|
|
184
|
+
|
|
185
|
+
async def refresh_token_if_needed(self) -> ClaudeCredentials | None:
|
|
186
|
+
"""Use enhanced base's automatic refresh capability."""
|
|
187
|
+
if await self.ensure_valid_token():
|
|
188
|
+
return await self.load_credentials()
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# ==================== Abstract Method Implementations ====================
|
|
192
|
+
|
|
193
|
+
async def refresh_token(self) -> ClaudeCredentials | None:
|
|
194
|
+
"""Refresh the access token using the refresh token.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Updated credentials or None if refresh failed
|
|
198
|
+
"""
|
|
199
|
+
# Load current credentials and extract refresh token
|
|
200
|
+
credentials = await self.load_credentials()
|
|
201
|
+
if not credentials:
|
|
202
|
+
logger.error("no_credentials_to_refresh", category="auth")
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
wrapper = ClaudeTokenWrapper(credentials=credentials)
|
|
206
|
+
refresh_token = wrapper.refresh_token_value
|
|
207
|
+
if not refresh_token:
|
|
208
|
+
logger.error("no_refresh_token_available", category="auth")
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
# Use injected provider or fallback to local import
|
|
213
|
+
new_credentials: ClaudeCredentials
|
|
214
|
+
if self.oauth_provider:
|
|
215
|
+
new_credentials = await self.oauth_provider.refresh_access_token(
|
|
216
|
+
refresh_token
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
# Fallback to local import if no provider injected
|
|
220
|
+
from .provider import ClaudeOAuthProvider
|
|
221
|
+
|
|
222
|
+
provider = ClaudeOAuthProvider(http_client=self.http_client)
|
|
223
|
+
new_credentials = await provider.refresh_access_token(refresh_token)
|
|
224
|
+
|
|
225
|
+
# Save updated credentials
|
|
226
|
+
if await self.save_credentials(new_credentials):
|
|
227
|
+
logger.info("token_refreshed_successfully", category="auth")
|
|
228
|
+
# Clear profile cache as token changed
|
|
229
|
+
self._profile_cache = None
|
|
230
|
+
|
|
231
|
+
return new_credentials
|
|
232
|
+
|
|
233
|
+
logger.error("failed_to_save_refreshed_credentials", category="auth")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(
|
|
238
|
+
"Token refresh failed",
|
|
239
|
+
error=str(e),
|
|
240
|
+
exc_info=e,
|
|
241
|
+
category="auth",
|
|
242
|
+
)
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def is_expired(self, credentials: ClaudeCredentials) -> bool:
|
|
246
|
+
"""Check if credentials are expired using wrapper."""
|
|
247
|
+
if isinstance(credentials, ClaudeCredentials):
|
|
248
|
+
wrapper = ClaudeTokenWrapper(credentials=credentials)
|
|
249
|
+
return bool(wrapper.is_expired)
|
|
250
|
+
|
|
251
|
+
expires_at = getattr(credentials, "expires_at", None)
|
|
252
|
+
if expires_at is None:
|
|
253
|
+
expires_at = getattr(credentials, "claude_ai_oauth", None)
|
|
254
|
+
if expires_at is not None:
|
|
255
|
+
expires_at = getattr(expires_at, "expires_at", None)
|
|
256
|
+
|
|
257
|
+
if expires_at is None:
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
if isinstance(expires_at, datetime):
|
|
261
|
+
return expires_at <= datetime.now(UTC)
|
|
262
|
+
if isinstance(expires_at, int | float):
|
|
263
|
+
return datetime.fromtimestamp(expires_at / 1000, tz=UTC) <= datetime.now(
|
|
264
|
+
UTC
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
# ==================== Targeted overrides ====================
|
|
270
|
+
|
|
271
|
+
async def load_credentials(self) -> ClaudeCredentials | None:
|
|
272
|
+
"""Load credentials and backfill subscription_type from profile if missing.
|
|
273
|
+
|
|
274
|
+
Avoids network calls; uses cached profile or local ~/.claude/.account.json
|
|
275
|
+
and writes back only when the field actually changes.
|
|
276
|
+
"""
|
|
277
|
+
creds = await super().load_credentials()
|
|
278
|
+
if not creds or not hasattr(creds, "claude_ai_oauth"):
|
|
279
|
+
return creds
|
|
280
|
+
|
|
281
|
+
sub = creds.claude_ai_oauth.subscription_type
|
|
282
|
+
if sub is None or str(sub).strip().lower() in {"", "unknown"}:
|
|
283
|
+
# Try cached profile first to avoid an extra file read
|
|
284
|
+
profile: ClaudeProfileInfo | None = self._profile_cache
|
|
285
|
+
if profile is None:
|
|
286
|
+
# Only read from disk if the profile file exists; no API calls here
|
|
287
|
+
try:
|
|
288
|
+
profile_storage = ClaudeProfileStorage()
|
|
289
|
+
if profile_storage.file_path.exists():
|
|
290
|
+
profile = await profile_storage.load_profile()
|
|
291
|
+
if profile:
|
|
292
|
+
self._profile_cache = profile
|
|
293
|
+
except Exception:
|
|
294
|
+
profile = None
|
|
295
|
+
|
|
296
|
+
if profile is not None:
|
|
297
|
+
try:
|
|
298
|
+
new_sub = self._derive_subscription_type(profile)
|
|
299
|
+
if new_sub != sub:
|
|
300
|
+
creds.claude_ai_oauth.subscription_type = new_sub
|
|
301
|
+
await self.save_credentials(creds)
|
|
302
|
+
logger.info(
|
|
303
|
+
"claude_subscription_type_backfilled_on_load",
|
|
304
|
+
subscription_type=new_sub,
|
|
305
|
+
category="auth",
|
|
306
|
+
)
|
|
307
|
+
except Exception as e:
|
|
308
|
+
logger.debug(
|
|
309
|
+
"claude_subscription_type_backfill_failed",
|
|
310
|
+
error=str(e),
|
|
311
|
+
category="auth",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return creds
|
|
315
|
+
|
|
316
|
+
def get_account_id(self, credentials: ClaudeCredentials) -> str | None:
|
|
317
|
+
"""Get account ID from credentials.
|
|
318
|
+
|
|
319
|
+
Claude doesn't store account_id in tokens, would need
|
|
320
|
+
to fetch from profile API.
|
|
321
|
+
"""
|
|
322
|
+
if self._profile_cache:
|
|
323
|
+
return self._profile_cache.account_id
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
# ==================== Claude-Specific Methods ====================
|
|
327
|
+
|
|
328
|
+
def get_expiration_time(self, credentials: ClaudeCredentials) -> datetime | None:
|
|
329
|
+
"""Get expiration time as datetime."""
|
|
330
|
+
wrapper = ClaudeTokenWrapper(credentials=credentials)
|
|
331
|
+
return wrapper.expires_at_datetime
|
|
332
|
+
|
|
333
|
+
async def get_profile_quick(self) -> ClaudeProfileInfo | None:
|
|
334
|
+
"""Return cached profile info only, avoiding I/O or network.
|
|
335
|
+
|
|
336
|
+
Profile cache is typically preloaded from local storage by
|
|
337
|
+
the async factory create() via preload_profile_cache().
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Cached ClaudeProfileInfo or None
|
|
341
|
+
"""
|
|
342
|
+
return self._profile_cache
|
|
343
|
+
|
|
344
|
+
async def get_access_token_value(self) -> str | None:
|
|
345
|
+
"""Get the actual access token value.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Access token string if available, None otherwise
|
|
349
|
+
"""
|
|
350
|
+
credentials = await self.load_credentials()
|
|
351
|
+
if not credentials:
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
if self.is_expired(credentials):
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
wrapper = ClaudeTokenWrapper(credentials=credentials)
|
|
358
|
+
return cast(str, wrapper.access_token_value)
|
|
359
|
+
|
|
360
|
+
async def get_profile(self) -> ClaudeProfileInfo | None:
|
|
361
|
+
"""Get user profile from cache or API.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
ClaudeProfileInfo or None if not authenticated
|
|
365
|
+
"""
|
|
366
|
+
if self._profile_cache:
|
|
367
|
+
return self._profile_cache
|
|
368
|
+
|
|
369
|
+
# Try to load from .account.json first
|
|
370
|
+
|
|
371
|
+
profile_storage = ClaudeProfileStorage()
|
|
372
|
+
profile = await profile_storage.load_profile()
|
|
373
|
+
if profile:
|
|
374
|
+
self._profile_cache = profile
|
|
375
|
+
# Best-effort sync of subscription type from cached profile
|
|
376
|
+
await self._sync_subscription_type_with_profile(profile)
|
|
377
|
+
return profile
|
|
378
|
+
|
|
379
|
+
# If not in storage, fetch from API
|
|
380
|
+
credentials = await self.load_credentials()
|
|
381
|
+
if not credentials or self.is_expired(credentials):
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
# Get access token
|
|
385
|
+
wrapper = ClaudeTokenWrapper(credentials=credentials)
|
|
386
|
+
access_token = cast(str, wrapper.access_token_value)
|
|
387
|
+
if not access_token:
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
# Fetch profile from API and save
|
|
391
|
+
try:
|
|
392
|
+
config = ClaudeOAuthConfig()
|
|
393
|
+
|
|
394
|
+
headers = {
|
|
395
|
+
"Authorization": f"Bearer {access_token}",
|
|
396
|
+
"Content-Type": "application/json",
|
|
397
|
+
}
|
|
398
|
+
# Optionally add detection headers if client supports it
|
|
399
|
+
try:
|
|
400
|
+
# Use injected provider or fallback to local import
|
|
401
|
+
if self.oauth_provider and hasattr(self.oauth_provider, "client"):
|
|
402
|
+
if hasattr(self.oauth_provider.client, "get_custom_headers"):
|
|
403
|
+
headers.update(self.oauth_provider.client.get_custom_headers())
|
|
404
|
+
else:
|
|
405
|
+
# Fallback to local import if no provider injected
|
|
406
|
+
from .provider import ClaudeOAuthProvider
|
|
407
|
+
|
|
408
|
+
temp_provider = ClaudeOAuthProvider(http_client=self.http_client)
|
|
409
|
+
if hasattr(temp_provider, "client") and hasattr(
|
|
410
|
+
temp_provider.client, "get_custom_headers"
|
|
411
|
+
):
|
|
412
|
+
headers.update(temp_provider.client.get_custom_headers())
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
# Debug logging for HTTP client usage
|
|
417
|
+
logger.debug(
|
|
418
|
+
"claude_manager_making_http_request",
|
|
419
|
+
url=config.profile_url,
|
|
420
|
+
http_client_id=id(self.http_client),
|
|
421
|
+
has_hooks=hasattr(self.http_client, "hook_manager")
|
|
422
|
+
and self.http_client.hook_manager is not None,
|
|
423
|
+
hook_manager_id=id(self.http_client.hook_manager)
|
|
424
|
+
if hasattr(self.http_client, "hook_manager")
|
|
425
|
+
and self.http_client.hook_manager
|
|
426
|
+
else None,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Use the injected HTTP client
|
|
430
|
+
response = await self.http_client.get(
|
|
431
|
+
config.profile_url,
|
|
432
|
+
headers=headers,
|
|
433
|
+
timeout=30.0,
|
|
434
|
+
)
|
|
435
|
+
response.raise_for_status()
|
|
436
|
+
|
|
437
|
+
profile_data = response.json()
|
|
438
|
+
|
|
439
|
+
# Save to .account.json
|
|
440
|
+
await profile_storage.save_profile(profile_data)
|
|
441
|
+
|
|
442
|
+
# Parse and cache
|
|
443
|
+
profile = ClaudeProfileInfo.from_api_response(profile_data)
|
|
444
|
+
self._profile_cache = profile
|
|
445
|
+
|
|
446
|
+
# Sync subscription type to credentials in a single write if changed
|
|
447
|
+
await self._sync_subscription_type_with_profile(
|
|
448
|
+
profile, credentials=credentials
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
logger.info(
|
|
452
|
+
"claude_profile_fetched_from_api",
|
|
453
|
+
account_id=profile.account_id,
|
|
454
|
+
email=profile.email,
|
|
455
|
+
category="auth",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
return profile
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
if isinstance(e, httpx.HTTPStatusError):
|
|
462
|
+
logger.error(
|
|
463
|
+
"claude_profile_api_error",
|
|
464
|
+
status_code=e.response.status_code,
|
|
465
|
+
error=str(e),
|
|
466
|
+
exc_info=e,
|
|
467
|
+
category="auth",
|
|
468
|
+
)
|
|
469
|
+
else:
|
|
470
|
+
logger.error(
|
|
471
|
+
"claude_profile_fetch_error",
|
|
472
|
+
error=str(e),
|
|
473
|
+
error_type=type(e).__name__,
|
|
474
|
+
exc_info=e,
|
|
475
|
+
category="auth",
|
|
476
|
+
)
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
async def close(self) -> None:
|
|
480
|
+
"""Close the HTTP client if it was created internally."""
|
|
481
|
+
if getattr(self, "_owns_client", False) and self.http_client:
|
|
482
|
+
await self.http_client.aclose()
|