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,166 +0,0 @@
|
|
|
1
|
-
"""OpenAI credentials management for Codex authentication."""
|
|
2
|
-
|
|
3
|
-
from datetime import UTC, datetime
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import jwt
|
|
7
|
-
import structlog
|
|
8
|
-
from pydantic import BaseModel, Field, field_validator
|
|
9
|
-
|
|
10
|
-
from .storage import OpenAITokenStorage
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
logger = structlog.get_logger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class OpenAICredentials(BaseModel):
|
|
17
|
-
"""OpenAI authentication credentials model."""
|
|
18
|
-
|
|
19
|
-
access_token: str = Field(..., description="OpenAI access token (JWT)")
|
|
20
|
-
refresh_token: str = Field(..., description="OpenAI refresh token")
|
|
21
|
-
expires_at: datetime = Field(..., description="Token expiration timestamp")
|
|
22
|
-
account_id: str = Field(..., description="OpenAI account ID extracted from token")
|
|
23
|
-
active: bool = Field(default=True, description="Whether credentials are active")
|
|
24
|
-
|
|
25
|
-
@field_validator("expires_at", mode="before")
|
|
26
|
-
@classmethod
|
|
27
|
-
def parse_expires_at(cls, v: Any) -> datetime:
|
|
28
|
-
"""Parse expiration timestamp."""
|
|
29
|
-
if isinstance(v, datetime):
|
|
30
|
-
# Ensure timezone-aware datetime
|
|
31
|
-
if v.tzinfo is None:
|
|
32
|
-
return v.replace(tzinfo=UTC)
|
|
33
|
-
return v
|
|
34
|
-
|
|
35
|
-
if isinstance(v, str):
|
|
36
|
-
# Handle ISO format strings
|
|
37
|
-
try:
|
|
38
|
-
dt = datetime.fromisoformat(v.replace("Z", "+00:00"))
|
|
39
|
-
if dt.tzinfo is None:
|
|
40
|
-
dt = dt.replace(tzinfo=UTC)
|
|
41
|
-
return dt
|
|
42
|
-
except ValueError as e:
|
|
43
|
-
raise ValueError(f"Invalid datetime format: {v}") from e
|
|
44
|
-
|
|
45
|
-
if isinstance(v, int | float):
|
|
46
|
-
# Handle Unix timestamps
|
|
47
|
-
return datetime.fromtimestamp(v, tz=UTC)
|
|
48
|
-
|
|
49
|
-
raise ValueError(f"Cannot parse datetime from {type(v)}: {v}")
|
|
50
|
-
|
|
51
|
-
@field_validator("account_id", mode="before")
|
|
52
|
-
@classmethod
|
|
53
|
-
def extract_account_id(cls, v: Any, info: Any) -> str:
|
|
54
|
-
"""Extract account ID from access token if not provided."""
|
|
55
|
-
if isinstance(v, str) and v:
|
|
56
|
-
return v
|
|
57
|
-
|
|
58
|
-
# Try to extract from access_token
|
|
59
|
-
access_token = None
|
|
60
|
-
if hasattr(info, "data") and info.data and isinstance(info.data, dict):
|
|
61
|
-
access_token = info.data.get("access_token")
|
|
62
|
-
|
|
63
|
-
if access_token and isinstance(access_token, str):
|
|
64
|
-
try:
|
|
65
|
-
# Decode JWT without verification to extract claims
|
|
66
|
-
decoded = jwt.decode(access_token, options={"verify_signature": False})
|
|
67
|
-
if "org_id" in decoded and isinstance(decoded["org_id"], str):
|
|
68
|
-
return decoded["org_id"]
|
|
69
|
-
elif "sub" in decoded and isinstance(decoded["sub"], str):
|
|
70
|
-
return decoded["sub"]
|
|
71
|
-
elif "account_id" in decoded and isinstance(decoded["account_id"], str):
|
|
72
|
-
return decoded["account_id"]
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.warning("Failed to extract account_id from token", error=str(e))
|
|
75
|
-
|
|
76
|
-
raise ValueError(
|
|
77
|
-
"account_id is required and could not be extracted from access_token"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
def is_expired(self) -> bool:
|
|
81
|
-
"""Check if the access token is expired."""
|
|
82
|
-
now = datetime.now(UTC)
|
|
83
|
-
return now >= self.expires_at
|
|
84
|
-
|
|
85
|
-
def expires_in_seconds(self) -> int:
|
|
86
|
-
"""Get seconds until token expires."""
|
|
87
|
-
now = datetime.now(UTC)
|
|
88
|
-
delta = self.expires_at - now
|
|
89
|
-
return max(0, int(delta.total_seconds()))
|
|
90
|
-
|
|
91
|
-
def to_dict(self) -> dict[str, Any]:
|
|
92
|
-
"""Convert to dictionary for storage."""
|
|
93
|
-
return {
|
|
94
|
-
"access_token": self.access_token,
|
|
95
|
-
"refresh_token": self.refresh_token,
|
|
96
|
-
"expires_at": self.expires_at.isoformat(),
|
|
97
|
-
"account_id": self.account_id,
|
|
98
|
-
"active": self.active,
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@classmethod
|
|
102
|
-
def from_dict(cls, data: dict[str, Any]) -> "OpenAICredentials":
|
|
103
|
-
"""Create from dictionary."""
|
|
104
|
-
return cls(**data)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class OpenAITokenManager:
|
|
108
|
-
"""Manages OpenAI token storage and refresh operations."""
|
|
109
|
-
|
|
110
|
-
def __init__(self, storage: OpenAITokenStorage | None = None):
|
|
111
|
-
"""Initialize token manager.
|
|
112
|
-
|
|
113
|
-
Args:
|
|
114
|
-
storage: Token storage backend. If None, uses default TOML file storage.
|
|
115
|
-
"""
|
|
116
|
-
self.storage = storage or OpenAITokenStorage()
|
|
117
|
-
|
|
118
|
-
async def load_credentials(self) -> OpenAICredentials | None:
|
|
119
|
-
"""Load credentials from storage."""
|
|
120
|
-
try:
|
|
121
|
-
return await self.storage.load()
|
|
122
|
-
except Exception as e:
|
|
123
|
-
logger.error("Failed to load OpenAI credentials", error=str(e))
|
|
124
|
-
return None
|
|
125
|
-
|
|
126
|
-
async def save_credentials(self, credentials: OpenAICredentials) -> bool:
|
|
127
|
-
"""Save credentials to storage."""
|
|
128
|
-
try:
|
|
129
|
-
return await self.storage.save(credentials)
|
|
130
|
-
except Exception as e:
|
|
131
|
-
logger.error("Failed to save OpenAI credentials", error=str(e))
|
|
132
|
-
return False
|
|
133
|
-
|
|
134
|
-
async def delete_credentials(self) -> bool:
|
|
135
|
-
"""Delete credentials from storage."""
|
|
136
|
-
try:
|
|
137
|
-
return await self.storage.delete()
|
|
138
|
-
except Exception as e:
|
|
139
|
-
logger.error("Failed to delete OpenAI credentials", error=str(e))
|
|
140
|
-
return False
|
|
141
|
-
|
|
142
|
-
async def has_credentials(self) -> bool:
|
|
143
|
-
"""Check if credentials exist."""
|
|
144
|
-
try:
|
|
145
|
-
return await self.storage.exists()
|
|
146
|
-
except Exception:
|
|
147
|
-
return False
|
|
148
|
-
|
|
149
|
-
async def get_valid_token(self) -> str | None:
|
|
150
|
-
"""Get a valid access token, refreshing if necessary."""
|
|
151
|
-
credentials = await self.load_credentials()
|
|
152
|
-
if not credentials or not credentials.active:
|
|
153
|
-
return None
|
|
154
|
-
|
|
155
|
-
# If token is not expired, return it
|
|
156
|
-
if not credentials.is_expired():
|
|
157
|
-
return credentials.access_token
|
|
158
|
-
|
|
159
|
-
# TODO: Implement token refresh logic
|
|
160
|
-
# For now, return None if expired (user needs to re-authenticate)
|
|
161
|
-
logger.warning("OpenAI token expired, refresh not yet implemented")
|
|
162
|
-
return None
|
|
163
|
-
|
|
164
|
-
def get_storage_location(self) -> str:
|
|
165
|
-
"""Get storage location description."""
|
|
166
|
-
return self.storage.get_location()
|
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
"""OpenAI OAuth PKCE client implementation."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import base64
|
|
5
|
-
import contextlib
|
|
6
|
-
import hashlib
|
|
7
|
-
import secrets
|
|
8
|
-
import urllib.parse
|
|
9
|
-
import webbrowser
|
|
10
|
-
from datetime import UTC, datetime, timedelta
|
|
11
|
-
|
|
12
|
-
import httpx
|
|
13
|
-
import structlog
|
|
14
|
-
import uvicorn
|
|
15
|
-
from fastapi import FastAPI, Request, Response
|
|
16
|
-
from fastapi.responses import HTMLResponse
|
|
17
|
-
|
|
18
|
-
from ccproxy.config.codex import CodexSettings
|
|
19
|
-
|
|
20
|
-
from .credentials import OpenAICredentials, OpenAITokenManager
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
logger = structlog.get_logger(__name__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class OpenAIOAuthClient:
|
|
27
|
-
"""OpenAI OAuth PKCE flow client."""
|
|
28
|
-
|
|
29
|
-
def __init__(
|
|
30
|
-
self, settings: CodexSettings, token_manager: OpenAITokenManager | None = None
|
|
31
|
-
):
|
|
32
|
-
"""Initialize OAuth client.
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
settings: Codex configuration settings
|
|
36
|
-
token_manager: Token manager for credential storage
|
|
37
|
-
"""
|
|
38
|
-
self.settings = settings
|
|
39
|
-
self.token_manager = token_manager or OpenAITokenManager()
|
|
40
|
-
self._server_task: asyncio.Task[None] | None = None
|
|
41
|
-
self._auth_complete = asyncio.Event()
|
|
42
|
-
self._auth_result: OpenAICredentials | None = None
|
|
43
|
-
self._auth_error: str | None = None
|
|
44
|
-
|
|
45
|
-
def _generate_pkce_pair(self) -> tuple[str, str]:
|
|
46
|
-
"""Generate PKCE code verifier and challenge."""
|
|
47
|
-
# Generate code verifier (43-128 characters)
|
|
48
|
-
code_verifier = (
|
|
49
|
-
base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip("=")
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
# Generate code challenge
|
|
53
|
-
code_challenge = (
|
|
54
|
-
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
|
|
55
|
-
.decode()
|
|
56
|
-
.rstrip("=")
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
return code_verifier, code_challenge
|
|
60
|
-
|
|
61
|
-
def _build_auth_url(self, code_challenge: str, state: str) -> str:
|
|
62
|
-
"""Build OAuth authorization URL."""
|
|
63
|
-
params = {
|
|
64
|
-
"response_type": "code",
|
|
65
|
-
"client_id": self.settings.oauth.client_id,
|
|
66
|
-
"redirect_uri": self.settings.get_redirect_uri(),
|
|
67
|
-
"scope": " ".join(self.settings.oauth.scopes),
|
|
68
|
-
"state": state,
|
|
69
|
-
"code_challenge": code_challenge,
|
|
70
|
-
"code_challenge_method": "S256",
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
query_string = urllib.parse.urlencode(params)
|
|
74
|
-
return f"{self.settings.oauth.base_url}/oauth/authorize?{query_string}"
|
|
75
|
-
|
|
76
|
-
async def _exchange_code_for_tokens(
|
|
77
|
-
self, code: str, code_verifier: str
|
|
78
|
-
) -> OpenAICredentials:
|
|
79
|
-
"""Exchange authorization code for tokens."""
|
|
80
|
-
token_url = f"{self.settings.oauth.base_url}/oauth/token"
|
|
81
|
-
|
|
82
|
-
data = {
|
|
83
|
-
"grant_type": "authorization_code",
|
|
84
|
-
"code": code,
|
|
85
|
-
"redirect_uri": self.settings.get_redirect_uri(),
|
|
86
|
-
"client_id": self.settings.oauth.client_id,
|
|
87
|
-
"code_verifier": code_verifier,
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
headers = {
|
|
91
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
92
|
-
"Accept": "application/json",
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async with httpx.AsyncClient() as client:
|
|
96
|
-
try:
|
|
97
|
-
response = await client.post(
|
|
98
|
-
token_url, data=data, headers=headers, timeout=30.0
|
|
99
|
-
)
|
|
100
|
-
response.raise_for_status()
|
|
101
|
-
|
|
102
|
-
token_data = response.json()
|
|
103
|
-
|
|
104
|
-
# Calculate expiration time
|
|
105
|
-
expires_in = token_data.get("expires_in", 3600) # Default 1 hour
|
|
106
|
-
expires_at = datetime.now(UTC).replace(microsecond=0) + timedelta(
|
|
107
|
-
seconds=expires_in
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
# Create credentials (account_id will be extracted from access_token)
|
|
111
|
-
credentials = OpenAICredentials(
|
|
112
|
-
access_token=token_data["access_token"],
|
|
113
|
-
refresh_token=token_data.get("refresh_token", ""),
|
|
114
|
-
expires_at=expires_at,
|
|
115
|
-
account_id="", # Will be auto-extracted by validator
|
|
116
|
-
active=True,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
return credentials
|
|
120
|
-
|
|
121
|
-
except httpx.HTTPStatusError as e:
|
|
122
|
-
error_detail = "Unknown error"
|
|
123
|
-
try:
|
|
124
|
-
error_data = e.response.json()
|
|
125
|
-
error_detail = error_data.get(
|
|
126
|
-
"error_description", error_data.get("error", str(e))
|
|
127
|
-
)
|
|
128
|
-
except Exception:
|
|
129
|
-
error_detail = str(e)
|
|
130
|
-
|
|
131
|
-
raise ValueError(f"Token exchange failed: {error_detail}") from e
|
|
132
|
-
except Exception as e:
|
|
133
|
-
raise ValueError(f"Token exchange request failed: {e}") from e
|
|
134
|
-
|
|
135
|
-
def _create_callback_app(self, code_verifier: str, expected_state: str) -> FastAPI:
|
|
136
|
-
"""Create FastAPI app to handle OAuth callback."""
|
|
137
|
-
app = FastAPI(title="OpenAI OAuth Callback")
|
|
138
|
-
|
|
139
|
-
@app.get("/auth/callback")
|
|
140
|
-
async def oauth_callback(request: Request) -> Response:
|
|
141
|
-
"""Handle OAuth callback."""
|
|
142
|
-
params = dict(request.query_params)
|
|
143
|
-
|
|
144
|
-
# Check for error in callback
|
|
145
|
-
if "error" in params:
|
|
146
|
-
error_desc = params.get("error_description", params["error"])
|
|
147
|
-
self._auth_error = f"OAuth error: {error_desc}"
|
|
148
|
-
self._auth_complete.set()
|
|
149
|
-
return HTMLResponse(
|
|
150
|
-
"""
|
|
151
|
-
<html>
|
|
152
|
-
<head><title>Authentication Failed</title></head>
|
|
153
|
-
<body>
|
|
154
|
-
<h1>Authentication Failed</h1>
|
|
155
|
-
<p>Error: """
|
|
156
|
-
+ error_desc
|
|
157
|
-
+ """</p>
|
|
158
|
-
<p>You can close this window.</p>
|
|
159
|
-
<script>setTimeout(() => window.close(), 3000);</script>
|
|
160
|
-
</body>
|
|
161
|
-
</html>
|
|
162
|
-
""",
|
|
163
|
-
status_code=400,
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
# Verify state parameter
|
|
167
|
-
received_state = params.get("state")
|
|
168
|
-
if received_state != expected_state:
|
|
169
|
-
self._auth_error = "Invalid state parameter"
|
|
170
|
-
self._auth_complete.set()
|
|
171
|
-
return HTMLResponse(
|
|
172
|
-
"""
|
|
173
|
-
<html>
|
|
174
|
-
<head><title>Authentication Failed</title></head>
|
|
175
|
-
<body>
|
|
176
|
-
<h1>Authentication Failed</h1>
|
|
177
|
-
<p>Invalid state parameter. Possible CSRF attack.</p>
|
|
178
|
-
<p>You can close this window.</p>
|
|
179
|
-
<script>setTimeout(() => window.close(), 3000);</script>
|
|
180
|
-
</body>
|
|
181
|
-
</html>
|
|
182
|
-
""",
|
|
183
|
-
status_code=400,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
# Get authorization code
|
|
187
|
-
auth_code = params.get("code")
|
|
188
|
-
if not auth_code:
|
|
189
|
-
self._auth_error = "No authorization code received"
|
|
190
|
-
self._auth_complete.set()
|
|
191
|
-
return HTMLResponse(
|
|
192
|
-
"""
|
|
193
|
-
<html>
|
|
194
|
-
<head><title>Authentication Failed</title></head>
|
|
195
|
-
<body>
|
|
196
|
-
<h1>Authentication Failed</h1>
|
|
197
|
-
<p>No authorization code received.</p>
|
|
198
|
-
<p>You can close this window.</p>
|
|
199
|
-
<script>setTimeout(() => window.close(), 3000);</script>
|
|
200
|
-
</body>
|
|
201
|
-
</html>
|
|
202
|
-
""",
|
|
203
|
-
status_code=400,
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
# Exchange code for tokens
|
|
207
|
-
try:
|
|
208
|
-
credentials = await self._exchange_code_for_tokens(
|
|
209
|
-
auth_code, code_verifier
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
# Save credentials
|
|
213
|
-
success = await self.token_manager.save_credentials(credentials)
|
|
214
|
-
if not success:
|
|
215
|
-
raise ValueError("Failed to save credentials")
|
|
216
|
-
|
|
217
|
-
self._auth_result = credentials
|
|
218
|
-
self._auth_complete.set()
|
|
219
|
-
|
|
220
|
-
return HTMLResponse(
|
|
221
|
-
"""
|
|
222
|
-
<html>
|
|
223
|
-
<head><title>Authentication Successful</title></head>
|
|
224
|
-
<body>
|
|
225
|
-
<h1>Authentication Successful!</h1>
|
|
226
|
-
<p>You have successfully authenticated with OpenAI.</p>
|
|
227
|
-
<p>You can close this window and return to the terminal.</p>
|
|
228
|
-
<script>setTimeout(() => window.close(), 3000);</script>
|
|
229
|
-
</body>
|
|
230
|
-
</html>
|
|
231
|
-
"""
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
except Exception as e:
|
|
235
|
-
logger.error("Token exchange failed", error=str(e))
|
|
236
|
-
self._auth_error = f"Token exchange failed: {e}"
|
|
237
|
-
self._auth_complete.set()
|
|
238
|
-
return HTMLResponse(
|
|
239
|
-
f"""
|
|
240
|
-
<html>
|
|
241
|
-
<head><title>Authentication Failed</title></head>
|
|
242
|
-
<body>
|
|
243
|
-
<h1>Authentication Failed</h1>
|
|
244
|
-
<p>Token exchange failed: {e}</p>
|
|
245
|
-
<p>You can close this window.</p>
|
|
246
|
-
<script>setTimeout(() => window.close(), 3000);</script>
|
|
247
|
-
</body>
|
|
248
|
-
</html>
|
|
249
|
-
""",
|
|
250
|
-
status_code=500,
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
return app
|
|
254
|
-
|
|
255
|
-
async def _run_callback_server(self, app: FastAPI) -> None:
|
|
256
|
-
"""Run callback server."""
|
|
257
|
-
config = uvicorn.Config(
|
|
258
|
-
app=app,
|
|
259
|
-
host="127.0.0.1",
|
|
260
|
-
port=self.settings.callback_port,
|
|
261
|
-
log_level="warning", # Reduce noise
|
|
262
|
-
access_log=False,
|
|
263
|
-
)
|
|
264
|
-
server = uvicorn.Server(config)
|
|
265
|
-
await server.serve()
|
|
266
|
-
|
|
267
|
-
async def authenticate(self, open_browser: bool = True) -> OpenAICredentials:
|
|
268
|
-
"""Perform OAuth PKCE flow.
|
|
269
|
-
|
|
270
|
-
Args:
|
|
271
|
-
open_browser: Whether to automatically open browser
|
|
272
|
-
|
|
273
|
-
Returns:
|
|
274
|
-
OpenAI credentials
|
|
275
|
-
|
|
276
|
-
Raises:
|
|
277
|
-
ValueError: If authentication fails
|
|
278
|
-
"""
|
|
279
|
-
# Reset state
|
|
280
|
-
self._auth_complete.clear()
|
|
281
|
-
self._auth_result = None
|
|
282
|
-
self._auth_error = None
|
|
283
|
-
|
|
284
|
-
# Generate PKCE parameters
|
|
285
|
-
code_verifier, code_challenge = self._generate_pkce_pair()
|
|
286
|
-
state = secrets.token_urlsafe(32)
|
|
287
|
-
|
|
288
|
-
# Create callback app
|
|
289
|
-
app = self._create_callback_app(code_verifier, state)
|
|
290
|
-
|
|
291
|
-
# Start callback server
|
|
292
|
-
self._server_task = asyncio.create_task(self._run_callback_server(app))
|
|
293
|
-
|
|
294
|
-
# Give server time to start
|
|
295
|
-
await asyncio.sleep(1)
|
|
296
|
-
|
|
297
|
-
# Build authorization URL
|
|
298
|
-
auth_url = self._build_auth_url(code_challenge, state)
|
|
299
|
-
|
|
300
|
-
logger.info("Starting OpenAI OAuth flow")
|
|
301
|
-
print("\nPlease visit this URL to authenticate with OpenAI:")
|
|
302
|
-
print(f"{auth_url}\n")
|
|
303
|
-
|
|
304
|
-
if open_browser:
|
|
305
|
-
try:
|
|
306
|
-
webbrowser.open(auth_url)
|
|
307
|
-
print("Opening browser...")
|
|
308
|
-
except Exception as e:
|
|
309
|
-
logger.warning("Failed to open browser automatically", error=str(e))
|
|
310
|
-
print("Please copy and paste the URL above into your browser.")
|
|
311
|
-
|
|
312
|
-
print("Waiting for authentication to complete...")
|
|
313
|
-
|
|
314
|
-
try:
|
|
315
|
-
# Wait for authentication to complete (with timeout)
|
|
316
|
-
await asyncio.wait_for(self._auth_complete.wait(), timeout=300) # 5 minutes
|
|
317
|
-
|
|
318
|
-
if self._auth_error:
|
|
319
|
-
raise ValueError(self._auth_error)
|
|
320
|
-
|
|
321
|
-
if not self._auth_result:
|
|
322
|
-
raise ValueError("Authentication completed but no credentials received")
|
|
323
|
-
|
|
324
|
-
logger.info("OpenAI authentication successful") # type: ignore[unreachable]
|
|
325
|
-
return self._auth_result
|
|
326
|
-
|
|
327
|
-
except TimeoutError as e:
|
|
328
|
-
raise ValueError("Authentication timed out (5 minutes)") from e
|
|
329
|
-
finally:
|
|
330
|
-
# Clean up server
|
|
331
|
-
if self._server_task and not self._server_task.done():
|
|
332
|
-
self._server_task.cancel()
|
|
333
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
334
|
-
await self._server_task
|
ccproxy/auth/openai/storage.py
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
"""JSON file storage for OpenAI credentials using Codex format."""
|
|
2
|
-
|
|
3
|
-
import contextlib
|
|
4
|
-
import json
|
|
5
|
-
from datetime import UTC, datetime
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
-
|
|
9
|
-
import jwt
|
|
10
|
-
import structlog
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from .credentials import OpenAICredentials
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
logger = structlog.get_logger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class OpenAITokenStorage:
|
|
21
|
-
"""JSON file-based storage for OpenAI credentials using Codex format."""
|
|
22
|
-
|
|
23
|
-
def __init__(self, file_path: Path | None = None):
|
|
24
|
-
"""Initialize storage with file path.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
file_path: Path to JSON file. If None, uses ~/.codex/auth.json
|
|
28
|
-
"""
|
|
29
|
-
self.file_path = file_path or Path.home() / ".codex" / "auth.json"
|
|
30
|
-
|
|
31
|
-
async def load(self) -> "OpenAICredentials | None":
|
|
32
|
-
"""Load credentials from Codex JSON file."""
|
|
33
|
-
if not self.file_path.exists():
|
|
34
|
-
return None
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
with self.file_path.open("r") as f:
|
|
38
|
-
data = json.load(f)
|
|
39
|
-
|
|
40
|
-
# Extract tokens section
|
|
41
|
-
tokens = data.get("tokens", {})
|
|
42
|
-
if not tokens:
|
|
43
|
-
logger.warning("No tokens section found in Codex auth file")
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
|
-
# Get required fields
|
|
47
|
-
access_token = tokens.get("access_token")
|
|
48
|
-
refresh_token = tokens.get("refresh_token")
|
|
49
|
-
account_id = tokens.get("account_id")
|
|
50
|
-
|
|
51
|
-
if not access_token:
|
|
52
|
-
logger.warning("No access_token found in Codex auth file")
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
# Extract expiration from JWT token
|
|
56
|
-
expires_at = self._extract_expiration_from_token(access_token)
|
|
57
|
-
if not expires_at:
|
|
58
|
-
logger.warning("Could not extract expiration from access token")
|
|
59
|
-
return None
|
|
60
|
-
|
|
61
|
-
# Import here to avoid circular import
|
|
62
|
-
from .credentials import OpenAICredentials
|
|
63
|
-
|
|
64
|
-
# Create credentials object
|
|
65
|
-
credentials_data = {
|
|
66
|
-
"access_token": access_token,
|
|
67
|
-
"refresh_token": refresh_token or "",
|
|
68
|
-
"expires_at": expires_at,
|
|
69
|
-
"account_id": account_id or "",
|
|
70
|
-
"active": True,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return OpenAICredentials.from_dict(credentials_data)
|
|
74
|
-
|
|
75
|
-
except Exception as e:
|
|
76
|
-
logger.error(
|
|
77
|
-
"Failed to load OpenAI credentials from Codex auth file",
|
|
78
|
-
file_path=str(self.file_path),
|
|
79
|
-
error=str(e),
|
|
80
|
-
)
|
|
81
|
-
return None
|
|
82
|
-
|
|
83
|
-
def _extract_expiration_from_token(self, access_token: str) -> datetime | None:
|
|
84
|
-
"""Extract expiration time from JWT access token."""
|
|
85
|
-
try:
|
|
86
|
-
decoded = jwt.decode(access_token, options={"verify_signature": False})
|
|
87
|
-
exp_timestamp = decoded.get("exp")
|
|
88
|
-
if exp_timestamp:
|
|
89
|
-
return datetime.fromtimestamp(exp_timestamp, tz=UTC)
|
|
90
|
-
except Exception as e:
|
|
91
|
-
logger.warning("Failed to decode JWT token for expiration", error=str(e))
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
async def save(self, credentials: "OpenAICredentials") -> bool:
|
|
95
|
-
"""Save credentials to Codex JSON file."""
|
|
96
|
-
try:
|
|
97
|
-
# Create directory if it doesn't exist
|
|
98
|
-
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
-
|
|
100
|
-
# Load existing file or create new structure
|
|
101
|
-
existing_data: dict[str, Any] = {}
|
|
102
|
-
if self.file_path.exists():
|
|
103
|
-
try:
|
|
104
|
-
with self.file_path.open("r") as f:
|
|
105
|
-
existing_data = json.load(f)
|
|
106
|
-
except Exception:
|
|
107
|
-
logger.warning(
|
|
108
|
-
"Could not load existing auth file, creating new one"
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
# Prepare Codex JSON data structure
|
|
112
|
-
codex_data = {
|
|
113
|
-
"OPENAI_API_KEY": existing_data.get("OPENAI_API_KEY"),
|
|
114
|
-
"tokens": {
|
|
115
|
-
"id_token": existing_data.get("tokens", {}).get("id_token"),
|
|
116
|
-
"access_token": credentials.access_token,
|
|
117
|
-
"refresh_token": credentials.refresh_token,
|
|
118
|
-
"account_id": credentials.account_id,
|
|
119
|
-
},
|
|
120
|
-
"last_refresh": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
# Write atomically by writing to temp file then renaming
|
|
124
|
-
temp_file = self.file_path.with_suffix(f"{self.file_path.suffix}.tmp")
|
|
125
|
-
|
|
126
|
-
with temp_file.open("w") as f:
|
|
127
|
-
json.dump(codex_data, f, indent=2)
|
|
128
|
-
|
|
129
|
-
# Set restrictive permissions (readable only by owner)
|
|
130
|
-
temp_file.chmod(0o600)
|
|
131
|
-
|
|
132
|
-
# Atomic rename
|
|
133
|
-
temp_file.replace(self.file_path)
|
|
134
|
-
|
|
135
|
-
logger.info(
|
|
136
|
-
"Saved OpenAI credentials to Codex auth file",
|
|
137
|
-
file_path=str(self.file_path),
|
|
138
|
-
)
|
|
139
|
-
return True
|
|
140
|
-
|
|
141
|
-
except Exception as e:
|
|
142
|
-
logger.error(
|
|
143
|
-
"Failed to save OpenAI credentials to Codex auth file",
|
|
144
|
-
file_path=str(self.file_path),
|
|
145
|
-
error=str(e),
|
|
146
|
-
)
|
|
147
|
-
# Clean up temp file if it exists
|
|
148
|
-
temp_file = self.file_path.with_suffix(f"{self.file_path.suffix}.tmp")
|
|
149
|
-
if temp_file.exists():
|
|
150
|
-
with contextlib.suppress(Exception):
|
|
151
|
-
temp_file.unlink()
|
|
152
|
-
return False
|
|
153
|
-
|
|
154
|
-
async def exists(self) -> bool:
|
|
155
|
-
"""Check if credentials file exists."""
|
|
156
|
-
if not self.file_path.exists():
|
|
157
|
-
return False
|
|
158
|
-
|
|
159
|
-
try:
|
|
160
|
-
with self.file_path.open("r") as f:
|
|
161
|
-
data = json.load(f)
|
|
162
|
-
tokens = data.get("tokens", {})
|
|
163
|
-
return bool(tokens.get("access_token"))
|
|
164
|
-
except Exception:
|
|
165
|
-
return False
|
|
166
|
-
|
|
167
|
-
async def delete(self) -> bool:
|
|
168
|
-
"""Delete credentials file."""
|
|
169
|
-
try:
|
|
170
|
-
if self.file_path.exists():
|
|
171
|
-
self.file_path.unlink()
|
|
172
|
-
logger.info("Deleted Codex auth file", file_path=str(self.file_path))
|
|
173
|
-
return True
|
|
174
|
-
except Exception as e:
|
|
175
|
-
logger.error(
|
|
176
|
-
"Failed to delete Codex auth file",
|
|
177
|
-
file_path=str(self.file_path),
|
|
178
|
-
error=str(e),
|
|
179
|
-
)
|
|
180
|
-
return False
|
|
181
|
-
|
|
182
|
-
def get_location(self) -> str:
|
|
183
|
-
"""Get storage location description."""
|
|
184
|
-
return str(self.file_path)
|