ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
"""OAuth client implementation for Anthropic OAuth flow."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import base64
|
|
5
|
-
import hashlib
|
|
6
|
-
import secrets
|
|
7
|
-
import urllib.parse
|
|
8
|
-
import webbrowser
|
|
9
|
-
from datetime import UTC, datetime
|
|
10
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
11
|
-
from threading import Thread
|
|
12
|
-
from typing import Any
|
|
13
|
-
from urllib.parse import parse_qs, urlparse
|
|
14
|
-
|
|
15
|
-
import httpx
|
|
16
|
-
from structlog import get_logger
|
|
17
|
-
|
|
18
|
-
from ccproxy.auth.exceptions import OAuthCallbackError, OAuthLoginError
|
|
19
|
-
from ccproxy.auth.models import ClaudeCredentials, OAuthToken, UserProfile
|
|
20
|
-
from ccproxy.auth.oauth.models import OAuthTokenRequest, OAuthTokenResponse
|
|
21
|
-
from ccproxy.config.auth import OAuthSettings
|
|
22
|
-
from ccproxy.services.credentials.config import OAuthConfig
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
logger = get_logger(__name__)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _log_http_error_compact(operation: str, response: httpx.Response) -> None:
|
|
29
|
-
"""Log HTTP error response in compact format.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
operation: Description of the operation that failed
|
|
33
|
-
response: HTTP response object
|
|
34
|
-
"""
|
|
35
|
-
import os
|
|
36
|
-
|
|
37
|
-
# Check if verbose API logging is enabled
|
|
38
|
-
verbose_api = os.environ.get("CCPROXY_VERBOSE_API", "false").lower() == "true"
|
|
39
|
-
|
|
40
|
-
if verbose_api:
|
|
41
|
-
# Full verbose logging
|
|
42
|
-
logger.error(
|
|
43
|
-
"http_operation_failed",
|
|
44
|
-
operation=operation,
|
|
45
|
-
status_code=response.status_code,
|
|
46
|
-
response_text=response.text,
|
|
47
|
-
)
|
|
48
|
-
else:
|
|
49
|
-
# Compact logging - truncate response body
|
|
50
|
-
response_text = response.text
|
|
51
|
-
if len(response_text) > 200:
|
|
52
|
-
response_preview = f"{response_text[:100]}...{response_text[-50:]}"
|
|
53
|
-
elif len(response_text) > 100:
|
|
54
|
-
response_preview = f"{response_text[:100]}..."
|
|
55
|
-
else:
|
|
56
|
-
response_preview = response_text
|
|
57
|
-
|
|
58
|
-
logger.error(
|
|
59
|
-
"http_operation_failed_compact",
|
|
60
|
-
operation=operation,
|
|
61
|
-
status_code=response.status_code,
|
|
62
|
-
response_preview=response_preview,
|
|
63
|
-
verbose_hint="use CCPROXY_VERBOSE_API=true for full response",
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class OAuthClient:
|
|
68
|
-
"""OAuth client for handling Anthropic OAuth flows."""
|
|
69
|
-
|
|
70
|
-
def __init__(self, config: OAuthSettings | None = None):
|
|
71
|
-
"""Initialize OAuth client.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
config: OAuth configuration, uses default if not provided
|
|
75
|
-
"""
|
|
76
|
-
self.config = config or OAuthConfig()
|
|
77
|
-
|
|
78
|
-
def generate_pkce_pair(self) -> tuple[str, str]:
|
|
79
|
-
"""Generate PKCE code verifier and challenge pair.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
Tuple of (code_verifier, code_challenge)
|
|
83
|
-
"""
|
|
84
|
-
# Generate code verifier (43-128 characters, URL-safe)
|
|
85
|
-
code_verifier = secrets.token_urlsafe(96) # 128 base64url chars
|
|
86
|
-
|
|
87
|
-
# For now, use plain method (Anthropic supports this)
|
|
88
|
-
# In production, should use SHA256 method
|
|
89
|
-
code_challenge = code_verifier
|
|
90
|
-
|
|
91
|
-
return code_verifier, code_challenge
|
|
92
|
-
|
|
93
|
-
def build_authorization_url(self, state: str, code_challenge: str) -> str:
|
|
94
|
-
"""Build authorization URL for OAuth flow.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
state: State parameter for CSRF protection
|
|
98
|
-
code_challenge: PKCE code challenge
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
Authorization URL
|
|
102
|
-
"""
|
|
103
|
-
params = {
|
|
104
|
-
"response_type": "code",
|
|
105
|
-
"client_id": self.config.client_id,
|
|
106
|
-
"redirect_uri": self.config.redirect_uri,
|
|
107
|
-
"scope": " ".join(self.config.scopes),
|
|
108
|
-
"state": state,
|
|
109
|
-
"code_challenge": code_challenge,
|
|
110
|
-
"code_challenge_method": "plain", # Using plain for simplicity
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
query_string = urllib.parse.urlencode(params)
|
|
114
|
-
return f"{self.config.authorize_url}?{query_string}"
|
|
115
|
-
|
|
116
|
-
async def exchange_code_for_tokens(
|
|
117
|
-
self,
|
|
118
|
-
authorization_code: str,
|
|
119
|
-
code_verifier: str,
|
|
120
|
-
) -> OAuthTokenResponse:
|
|
121
|
-
"""Exchange authorization code for access tokens.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
authorization_code: Authorization code from callback
|
|
125
|
-
code_verifier: PKCE code verifier
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
Token response
|
|
129
|
-
|
|
130
|
-
Raises:
|
|
131
|
-
httpx.HTTPError: If token exchange fails
|
|
132
|
-
"""
|
|
133
|
-
token_request = OAuthTokenRequest(
|
|
134
|
-
code=authorization_code,
|
|
135
|
-
redirect_uri=self.config.redirect_uri,
|
|
136
|
-
client_id=self.config.client_id,
|
|
137
|
-
code_verifier=code_verifier,
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
headers = {
|
|
141
|
-
"Content-Type": "application/json",
|
|
142
|
-
"anthropic-beta": self.config.beta_version,
|
|
143
|
-
"User-Agent": self.config.user_agent,
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async with httpx.AsyncClient() as client:
|
|
147
|
-
response = await client.post(
|
|
148
|
-
self.config.token_url,
|
|
149
|
-
headers=headers,
|
|
150
|
-
json=token_request.model_dump(),
|
|
151
|
-
timeout=self.config.request_timeout,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
if response.status_code != 200:
|
|
155
|
-
_log_http_error_compact("Token exchange", response)
|
|
156
|
-
response.raise_for_status()
|
|
157
|
-
|
|
158
|
-
data = response.json()
|
|
159
|
-
return OAuthTokenResponse.model_validate(data)
|
|
160
|
-
|
|
161
|
-
async def refresh_access_token(self, refresh_token: str) -> OAuthTokenResponse:
|
|
162
|
-
"""Refresh access token using refresh token.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
refresh_token: Refresh token
|
|
166
|
-
|
|
167
|
-
Returns:
|
|
168
|
-
New token response
|
|
169
|
-
|
|
170
|
-
Raises:
|
|
171
|
-
httpx.HTTPError: If token refresh fails
|
|
172
|
-
"""
|
|
173
|
-
refresh_request = {
|
|
174
|
-
"grant_type": "refresh_token",
|
|
175
|
-
"refresh_token": refresh_token,
|
|
176
|
-
"client_id": self.config.client_id,
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
headers = {
|
|
180
|
-
"Content-Type": "application/json",
|
|
181
|
-
"anthropic-beta": self.config.beta_version,
|
|
182
|
-
"User-Agent": self.config.user_agent,
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async with httpx.AsyncClient() as client:
|
|
186
|
-
response = await client.post(
|
|
187
|
-
self.config.token_url,
|
|
188
|
-
headers=headers,
|
|
189
|
-
json=refresh_request,
|
|
190
|
-
timeout=self.config.request_timeout,
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
if response.status_code != 200:
|
|
194
|
-
_log_http_error_compact("Token refresh", response)
|
|
195
|
-
response.raise_for_status()
|
|
196
|
-
|
|
197
|
-
data = response.json()
|
|
198
|
-
return OAuthTokenResponse.model_validate(data)
|
|
199
|
-
|
|
200
|
-
async def refresh_token(self, refresh_token: str) -> "OAuthToken":
|
|
201
|
-
"""Refresh token using refresh token - compatibility method for tests.
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
refresh_token: Refresh token
|
|
205
|
-
|
|
206
|
-
Returns:
|
|
207
|
-
New OAuth token
|
|
208
|
-
|
|
209
|
-
Raises:
|
|
210
|
-
OAuthTokenRefreshError: If token refresh fails
|
|
211
|
-
"""
|
|
212
|
-
from datetime import UTC, datetime
|
|
213
|
-
|
|
214
|
-
from ccproxy.auth.exceptions import OAuthTokenRefreshError
|
|
215
|
-
from ccproxy.auth.models import OAuthToken
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
token_response = await self.refresh_access_token(refresh_token)
|
|
219
|
-
|
|
220
|
-
expires_in = (
|
|
221
|
-
token_response.expires_in if token_response.expires_in else 3600
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
# Convert to OAuthToken format expected by tests
|
|
225
|
-
expires_at_ms = int((datetime.now(UTC).timestamp() + expires_in) * 1000)
|
|
226
|
-
|
|
227
|
-
return OAuthToken(
|
|
228
|
-
accessToken=token_response.access_token,
|
|
229
|
-
refreshToken=token_response.refresh_token or refresh_token,
|
|
230
|
-
expiresAt=expires_at_ms,
|
|
231
|
-
scopes=token_response.scope.split() if token_response.scope else [],
|
|
232
|
-
subscriptionType="pro", # Default value
|
|
233
|
-
)
|
|
234
|
-
except Exception as e:
|
|
235
|
-
raise OAuthTokenRefreshError(f"Token refresh failed: {e}") from e
|
|
236
|
-
|
|
237
|
-
async def fetch_user_profile(self, access_token: str) -> UserProfile | None:
|
|
238
|
-
"""Fetch user profile information using access token.
|
|
239
|
-
|
|
240
|
-
Args:
|
|
241
|
-
access_token: Valid OAuth access token
|
|
242
|
-
|
|
243
|
-
Returns:
|
|
244
|
-
User profile information
|
|
245
|
-
|
|
246
|
-
Raises:
|
|
247
|
-
httpx.HTTPError: If profile fetch fails
|
|
248
|
-
"""
|
|
249
|
-
from ccproxy.auth.models import UserProfile
|
|
250
|
-
|
|
251
|
-
headers = {
|
|
252
|
-
"Authorization": f"Bearer {access_token}",
|
|
253
|
-
"anthropic-beta": self.config.beta_version,
|
|
254
|
-
"User-Agent": self.config.user_agent,
|
|
255
|
-
"Content-Type": "application/json",
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
# Use the profile url
|
|
259
|
-
async with httpx.AsyncClient() as client:
|
|
260
|
-
response = await client.get(
|
|
261
|
-
self.config.profile_url,
|
|
262
|
-
headers=headers,
|
|
263
|
-
timeout=self.config.request_timeout,
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
if response.status_code == 404:
|
|
267
|
-
# Userinfo endpoint not available - this is expected for some OAuth providers
|
|
268
|
-
logger.debug(
|
|
269
|
-
"userinfo_endpoint_unavailable", endpoint=self.config.profile_url
|
|
270
|
-
)
|
|
271
|
-
return None
|
|
272
|
-
elif response.status_code != 200:
|
|
273
|
-
_log_http_error_compact("Profile fetch", response)
|
|
274
|
-
response.raise_for_status()
|
|
275
|
-
|
|
276
|
-
data = response.json()
|
|
277
|
-
logger.debug("user_profile_fetched", endpoint=self.config.profile_url)
|
|
278
|
-
return UserProfile.model_validate(data)
|
|
279
|
-
|
|
280
|
-
async def login(self) -> ClaudeCredentials:
|
|
281
|
-
"""Perform OAuth login flow.
|
|
282
|
-
|
|
283
|
-
Returns:
|
|
284
|
-
ClaudeCredentials with OAuth token
|
|
285
|
-
|
|
286
|
-
Raises:
|
|
287
|
-
OAuthLoginError: If login fails
|
|
288
|
-
OAuthCallbackError: If callback processing fails
|
|
289
|
-
"""
|
|
290
|
-
# Generate state parameter for security
|
|
291
|
-
state = secrets.token_urlsafe(32)
|
|
292
|
-
|
|
293
|
-
# Generate PKCE parameters
|
|
294
|
-
code_verifier = secrets.token_urlsafe(32)
|
|
295
|
-
code_challenge = (
|
|
296
|
-
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
|
|
297
|
-
.decode()
|
|
298
|
-
.rstrip("=")
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
authorization_code = None
|
|
302
|
-
error = None
|
|
303
|
-
|
|
304
|
-
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
305
|
-
def do_GET(self) -> None: # noqa: N802
|
|
306
|
-
nonlocal authorization_code, error
|
|
307
|
-
|
|
308
|
-
# Ignore favicon requests
|
|
309
|
-
if self.path == "/favicon.ico":
|
|
310
|
-
self.send_response(404)
|
|
311
|
-
self.end_headers()
|
|
312
|
-
return
|
|
313
|
-
|
|
314
|
-
parsed_url = urlparse(self.path)
|
|
315
|
-
query_params = parse_qs(parsed_url.query)
|
|
316
|
-
|
|
317
|
-
# Check state parameter
|
|
318
|
-
received_state = query_params.get("state", [None])[0]
|
|
319
|
-
|
|
320
|
-
if received_state != state:
|
|
321
|
-
error = "Invalid state parameter"
|
|
322
|
-
self.send_response(400)
|
|
323
|
-
self.end_headers()
|
|
324
|
-
self.wfile.write(b"Error: Invalid state parameter")
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
# Check for authorization code
|
|
328
|
-
if "code" in query_params:
|
|
329
|
-
authorization_code = query_params["code"][0]
|
|
330
|
-
self.send_response(200)
|
|
331
|
-
self.end_headers()
|
|
332
|
-
self.wfile.write(b"Login successful! You can close this window.")
|
|
333
|
-
elif "error" in query_params:
|
|
334
|
-
error = query_params.get("error_description", ["Unknown error"])[0]
|
|
335
|
-
self.send_response(400)
|
|
336
|
-
self.end_headers()
|
|
337
|
-
self.wfile.write(f"Error: {error}".encode())
|
|
338
|
-
else:
|
|
339
|
-
error = "No authorization code received"
|
|
340
|
-
self.send_response(400)
|
|
341
|
-
self.end_headers()
|
|
342
|
-
self.wfile.write(b"Error: No authorization code received")
|
|
343
|
-
|
|
344
|
-
def log_message(self, format: str, *args: Any) -> None:
|
|
345
|
-
# Suppress HTTP server logs
|
|
346
|
-
pass
|
|
347
|
-
|
|
348
|
-
# Start local HTTP server for OAuth callback
|
|
349
|
-
server = HTTPServer(
|
|
350
|
-
("localhost", self.config.callback_port), OAuthCallbackHandler
|
|
351
|
-
)
|
|
352
|
-
server_thread = Thread(target=server.serve_forever)
|
|
353
|
-
server_thread.daemon = True
|
|
354
|
-
server_thread.start()
|
|
355
|
-
|
|
356
|
-
try:
|
|
357
|
-
# Build authorization URL
|
|
358
|
-
auth_params = {
|
|
359
|
-
"response_type": "code",
|
|
360
|
-
"client_id": self.config.client_id,
|
|
361
|
-
"redirect_uri": self.config.redirect_uri,
|
|
362
|
-
"scope": " ".join(self.config.scopes),
|
|
363
|
-
"state": state,
|
|
364
|
-
"code_challenge": code_challenge,
|
|
365
|
-
"code_challenge_method": "S256",
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
auth_url = (
|
|
369
|
-
f"{self.config.authorize_url}?{urllib.parse.urlencode(auth_params)}"
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
logger.info("oauth_browser_opening", auth_url=auth_url)
|
|
373
|
-
logger.info(
|
|
374
|
-
"oauth_manual_url",
|
|
375
|
-
message="If browser doesn't open, visit this URL",
|
|
376
|
-
auth_url=auth_url,
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
# Open browser
|
|
380
|
-
webbrowser.open(auth_url)
|
|
381
|
-
|
|
382
|
-
# Wait for callback (with timeout)
|
|
383
|
-
import time
|
|
384
|
-
|
|
385
|
-
start_time = time.time()
|
|
386
|
-
|
|
387
|
-
while authorization_code is None and error is None:
|
|
388
|
-
if time.time() - start_time > self.config.callback_timeout:
|
|
389
|
-
error = "Login timeout"
|
|
390
|
-
break
|
|
391
|
-
await asyncio.sleep(0.1)
|
|
392
|
-
|
|
393
|
-
if error:
|
|
394
|
-
raise OAuthCallbackError(f"OAuth callback failed: {error}")
|
|
395
|
-
|
|
396
|
-
if not authorization_code:
|
|
397
|
-
raise OAuthLoginError("No authorization code received")
|
|
398
|
-
|
|
399
|
-
# Exchange authorization code for tokens
|
|
400
|
-
token_data = {
|
|
401
|
-
"grant_type": "authorization_code",
|
|
402
|
-
"code": authorization_code,
|
|
403
|
-
"redirect_uri": self.config.redirect_uri,
|
|
404
|
-
"client_id": self.config.client_id,
|
|
405
|
-
"code_verifier": code_verifier,
|
|
406
|
-
"state": state,
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
headers = {
|
|
410
|
-
"Content-Type": "application/json",
|
|
411
|
-
"anthropic-beta": self.config.beta_version,
|
|
412
|
-
"User-Agent": self.config.user_agent,
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
async with httpx.AsyncClient() as client:
|
|
416
|
-
response = await client.post(
|
|
417
|
-
self.config.token_url,
|
|
418
|
-
headers=headers,
|
|
419
|
-
json=token_data,
|
|
420
|
-
timeout=30.0,
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
if response.status_code == 200:
|
|
424
|
-
result = response.json()
|
|
425
|
-
|
|
426
|
-
# Calculate expires_at from expires_in
|
|
427
|
-
expires_in = result.get("expires_in")
|
|
428
|
-
expires_at = None
|
|
429
|
-
if expires_in:
|
|
430
|
-
expires_at = int(
|
|
431
|
-
(datetime.now(UTC).timestamp() + expires_in) * 1000
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
# Create credentials object
|
|
435
|
-
oauth_data = {
|
|
436
|
-
"accessToken": result.get("access_token"),
|
|
437
|
-
"refreshToken": result.get("refresh_token"),
|
|
438
|
-
"expiresAt": expires_at,
|
|
439
|
-
"scopes": result.get("scope", "").split()
|
|
440
|
-
if result.get("scope")
|
|
441
|
-
else self.config.scopes,
|
|
442
|
-
"subscriptionType": result.get("subscription_type", "unknown"),
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
credentials = ClaudeCredentials(claudeAiOauth=OAuthToken(**oauth_data))
|
|
446
|
-
|
|
447
|
-
logger.info("oauth_login_completed", client_id=self.config.client_id)
|
|
448
|
-
return credentials
|
|
449
|
-
|
|
450
|
-
else:
|
|
451
|
-
# Use compact logging for the error message
|
|
452
|
-
import os
|
|
453
|
-
|
|
454
|
-
verbose_api = (
|
|
455
|
-
os.environ.get("CCPROXY_VERBOSE_API", "false").lower() == "true"
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
if verbose_api:
|
|
459
|
-
error_detail = response.text
|
|
460
|
-
else:
|
|
461
|
-
response_text = response.text
|
|
462
|
-
if len(response_text) > 200:
|
|
463
|
-
error_detail = f"{response_text[:100]}...{response_text[-50:]}"
|
|
464
|
-
elif len(response_text) > 100:
|
|
465
|
-
error_detail = f"{response_text[:100]}..."
|
|
466
|
-
else:
|
|
467
|
-
error_detail = response_text
|
|
468
|
-
|
|
469
|
-
raise OAuthLoginError(
|
|
470
|
-
f"Token exchange failed: {response.status_code} - {error_detail}"
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
except Exception as e:
|
|
474
|
-
if isinstance(e, OAuthLoginError | OAuthCallbackError):
|
|
475
|
-
raise
|
|
476
|
-
raise OAuthLoginError(f"OAuth login failed: {e}") from e
|
|
477
|
-
|
|
478
|
-
finally:
|
|
479
|
-
# Stop the HTTP server
|
|
480
|
-
server.shutdown()
|
|
481
|
-
server_thread.join(timeout=1)
|