ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"""Codex/OpenAI OAuth provider for plugin registration."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from base64 import urlsafe_b64encode
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ccproxy.auth.oauth.protocol import ProfileLoggingMixin, StandardProfileFields
|
|
11
|
+
from ccproxy.auth.oauth.registry import CliAuthConfig, FlowType, OAuthProviderInfo
|
|
12
|
+
from ccproxy.config.settings import Settings
|
|
13
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
14
|
+
|
|
15
|
+
from .client import CodexOAuthClient
|
|
16
|
+
from .config import CodexOAuthConfig
|
|
17
|
+
from .models import OpenAICredentials
|
|
18
|
+
from .storage import CodexTokenStorage
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = get_plugin_logger()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CodexOAuthProvider(ProfileLoggingMixin):
|
|
25
|
+
"""Codex/OpenAI OAuth provider implementation for registry."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
config: CodexOAuthConfig | None = None,
|
|
30
|
+
storage: CodexTokenStorage | None = None,
|
|
31
|
+
http_client: httpx.AsyncClient | None = None,
|
|
32
|
+
hook_manager: Any | None = None,
|
|
33
|
+
settings: Settings | None = None,
|
|
34
|
+
):
|
|
35
|
+
"""Initialize Codex OAuth provider.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: OAuth configuration
|
|
39
|
+
storage: Token storage
|
|
40
|
+
http_client: Optional HTTP client (for request tracing support)
|
|
41
|
+
hook_manager: Optional hook manager for emitting events
|
|
42
|
+
settings: Optional settings for HTTP client configuration
|
|
43
|
+
"""
|
|
44
|
+
self.config = config or CodexOAuthConfig()
|
|
45
|
+
self.storage = storage or CodexTokenStorage()
|
|
46
|
+
self.hook_manager = hook_manager
|
|
47
|
+
self.http_client = http_client
|
|
48
|
+
self.settings = settings
|
|
49
|
+
|
|
50
|
+
self.client = CodexOAuthClient(
|
|
51
|
+
self.config,
|
|
52
|
+
self.storage,
|
|
53
|
+
http_client,
|
|
54
|
+
hook_manager=hook_manager,
|
|
55
|
+
settings=settings,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def provider_name(self) -> str:
|
|
60
|
+
"""Internal provider name."""
|
|
61
|
+
return "codex"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def provider_display_name(self) -> str:
|
|
65
|
+
"""Display name for UI."""
|
|
66
|
+
return "OpenAI Codex"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def supports_pkce(self) -> bool:
|
|
70
|
+
"""Whether this provider supports PKCE."""
|
|
71
|
+
return self.config.use_pkce
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def supports_refresh(self) -> bool:
|
|
75
|
+
"""Whether this provider supports token refresh."""
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def requires_client_secret(self) -> bool:
|
|
80
|
+
"""Whether this provider requires a client secret."""
|
|
81
|
+
return False # OpenAI uses PKCE flow without client secret
|
|
82
|
+
|
|
83
|
+
async def get_authorization_url(
|
|
84
|
+
self,
|
|
85
|
+
state: str,
|
|
86
|
+
code_verifier: str | None = None,
|
|
87
|
+
redirect_uri: str | None = None,
|
|
88
|
+
) -> str:
|
|
89
|
+
"""Get the authorization URL for OAuth flow.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
state: OAuth state parameter for CSRF protection
|
|
93
|
+
code_verifier: PKCE code verifier (if PKCE is supported)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Authorization URL to redirect user to
|
|
97
|
+
"""
|
|
98
|
+
params = {
|
|
99
|
+
"response_type": "code",
|
|
100
|
+
"client_id": self.config.client_id,
|
|
101
|
+
"redirect_uri": redirect_uri or self.config.get_redirect_uri(),
|
|
102
|
+
"scope": " ".join(self.config.scopes),
|
|
103
|
+
"state": state,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Add PKCE challenge if supported and verifier provided
|
|
107
|
+
if self.config.use_pkce and code_verifier:
|
|
108
|
+
code_challenge = (
|
|
109
|
+
urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
|
|
110
|
+
.decode()
|
|
111
|
+
.rstrip("=")
|
|
112
|
+
)
|
|
113
|
+
params["code_challenge"] = code_challenge
|
|
114
|
+
params["code_challenge_method"] = "S256"
|
|
115
|
+
|
|
116
|
+
auth_url = f"{self.config.authorize_url}?{urlencode(params)}"
|
|
117
|
+
|
|
118
|
+
logger.info(
|
|
119
|
+
"codex_oauth_auth_url_generated",
|
|
120
|
+
state=state,
|
|
121
|
+
has_pkce=bool(code_verifier and self.config.use_pkce),
|
|
122
|
+
category="auth",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return auth_url
|
|
126
|
+
|
|
127
|
+
async def handle_callback(
|
|
128
|
+
self,
|
|
129
|
+
code: str,
|
|
130
|
+
state: str,
|
|
131
|
+
code_verifier: str | None = None,
|
|
132
|
+
redirect_uri: str | None = None,
|
|
133
|
+
) -> Any:
|
|
134
|
+
"""Handle OAuth callback and exchange code for tokens.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
code: Authorization code from OAuth callback
|
|
138
|
+
state: State parameter for validation
|
|
139
|
+
code_verifier: PKCE code verifier (if PKCE is used)
|
|
140
|
+
redirect_uri: Redirect URI used in authorization (optional)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
OpenAI credentials object
|
|
144
|
+
"""
|
|
145
|
+
# Use the client's handle_callback method which includes code exchange
|
|
146
|
+
# If a specific redirect_uri was provided, create a temporary client with that URI
|
|
147
|
+
if redirect_uri and redirect_uri != self.client.redirect_uri:
|
|
148
|
+
# Create temporary config with the specific redirect URI
|
|
149
|
+
temp_config = CodexOAuthConfig(
|
|
150
|
+
client_id=self.config.client_id,
|
|
151
|
+
redirect_uri=redirect_uri,
|
|
152
|
+
scopes=self.config.scopes,
|
|
153
|
+
base_url=self.config.base_url,
|
|
154
|
+
authorize_url=self.config.authorize_url,
|
|
155
|
+
token_url=self.config.token_url,
|
|
156
|
+
audience=self.config.audience,
|
|
157
|
+
use_pkce=self.config.use_pkce,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Create temporary client with the correct redirect URI
|
|
161
|
+
temp_client = CodexOAuthClient(
|
|
162
|
+
temp_config,
|
|
163
|
+
self.storage,
|
|
164
|
+
self.http_client,
|
|
165
|
+
hook_manager=self.hook_manager,
|
|
166
|
+
settings=self.settings,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
credentials = await temp_client.handle_callback(
|
|
170
|
+
code, state, code_verifier or ""
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
# Use the regular client
|
|
174
|
+
credentials = await self.client.handle_callback(
|
|
175
|
+
code, state, code_verifier or ""
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# The client already saves to storage if available, but we can save again
|
|
179
|
+
# to our specific storage if needed
|
|
180
|
+
if self.storage:
|
|
181
|
+
await self.storage.save(credentials)
|
|
182
|
+
|
|
183
|
+
logger.info(
|
|
184
|
+
"codex_oauth_callback_handled",
|
|
185
|
+
state=state,
|
|
186
|
+
has_credentials=bool(credentials),
|
|
187
|
+
has_id_token=bool(credentials.id_token),
|
|
188
|
+
category="auth",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return credentials
|
|
192
|
+
|
|
193
|
+
async def refresh_access_token(self, refresh_token: str) -> Any:
|
|
194
|
+
"""Refresh access token using refresh token.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
refresh_token: Refresh token from previous auth
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
New token response
|
|
201
|
+
"""
|
|
202
|
+
credentials = await self.client.refresh_token(refresh_token)
|
|
203
|
+
|
|
204
|
+
# Store updated credentials
|
|
205
|
+
if self.storage:
|
|
206
|
+
await self.storage.save(credentials)
|
|
207
|
+
|
|
208
|
+
logger.info("codex_oauth_token_refreshed", category="auth")
|
|
209
|
+
|
|
210
|
+
return credentials
|
|
211
|
+
|
|
212
|
+
async def revoke_token(self, token: str) -> None:
|
|
213
|
+
"""Revoke an access or refresh token.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
token: Token to revoke
|
|
217
|
+
"""
|
|
218
|
+
# OpenAI doesn't have a revoke endpoint, so we just delete stored credentials
|
|
219
|
+
if self.storage:
|
|
220
|
+
await self.storage.delete()
|
|
221
|
+
|
|
222
|
+
logger.info("codex_oauth_token_revoked_locally", category="auth")
|
|
223
|
+
|
|
224
|
+
def get_provider_info(self) -> OAuthProviderInfo:
|
|
225
|
+
"""Get provider information for discovery.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Provider information
|
|
229
|
+
"""
|
|
230
|
+
return OAuthProviderInfo(
|
|
231
|
+
name=self.provider_name,
|
|
232
|
+
display_name=self.provider_display_name,
|
|
233
|
+
description="OAuth authentication for OpenAI Codex",
|
|
234
|
+
supports_pkce=self.supports_pkce,
|
|
235
|
+
scopes=self.config.scopes,
|
|
236
|
+
is_available=True,
|
|
237
|
+
plugin_name="oauth_codex",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
async def validate_token(self, access_token: str) -> bool:
|
|
241
|
+
"""Validate an access token.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
access_token: Token to validate
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True if token is valid
|
|
248
|
+
"""
|
|
249
|
+
# OpenAI doesn't have a validation endpoint, so we check if stored token matches
|
|
250
|
+
if self.storage:
|
|
251
|
+
credentials = await self.storage.load()
|
|
252
|
+
if credentials:
|
|
253
|
+
return credentials.access_token == access_token
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
async def get_user_info(self, access_token: str) -> dict[str, Any] | None:
|
|
257
|
+
"""Get user information using access token.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
access_token: Valid access token
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
User information or None
|
|
264
|
+
"""
|
|
265
|
+
# Load stored credentials
|
|
266
|
+
if self.storage:
|
|
267
|
+
credentials = await self.storage.load()
|
|
268
|
+
if credentials:
|
|
269
|
+
info = {
|
|
270
|
+
"account_id": credentials.account_id,
|
|
271
|
+
"active": credentials.active,
|
|
272
|
+
"has_id_token": bool(credentials.id_token),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Try to extract info from ID token if present
|
|
276
|
+
if credentials.id_token:
|
|
277
|
+
try:
|
|
278
|
+
import jwt
|
|
279
|
+
|
|
280
|
+
decoded = jwt.decode(
|
|
281
|
+
credentials.id_token,
|
|
282
|
+
options={"verify_signature": False},
|
|
283
|
+
)
|
|
284
|
+
info.update(
|
|
285
|
+
{
|
|
286
|
+
"email": decoded.get("email"),
|
|
287
|
+
"name": decoded.get("name"),
|
|
288
|
+
"sub": decoded.get("sub"),
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
return info
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
def get_storage(self) -> Any:
|
|
298
|
+
"""Get storage implementation for this provider.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Storage implementation
|
|
302
|
+
"""
|
|
303
|
+
return self.storage
|
|
304
|
+
|
|
305
|
+
def get_config(self) -> Any:
|
|
306
|
+
"""Get configuration for this provider.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Configuration implementation
|
|
310
|
+
"""
|
|
311
|
+
return self.config
|
|
312
|
+
|
|
313
|
+
async def save_credentials(
|
|
314
|
+
self, credentials: Any, custom_path: Any | None = None
|
|
315
|
+
) -> bool:
|
|
316
|
+
"""Save credentials using provider's storage mechanism.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
credentials: OpenAI credentials object
|
|
320
|
+
custom_path: Optional custom storage path (Path object)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
True if saved successfully, False otherwise
|
|
324
|
+
"""
|
|
325
|
+
from pathlib import Path
|
|
326
|
+
|
|
327
|
+
from ccproxy.auth.storage.generic import GenericJsonStorage
|
|
328
|
+
|
|
329
|
+
from .manager import CodexTokenManager
|
|
330
|
+
from .models import OpenAICredentials
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
if custom_path:
|
|
334
|
+
# Use custom path for storage
|
|
335
|
+
storage = GenericJsonStorage(Path(custom_path), OpenAICredentials)
|
|
336
|
+
manager = await CodexTokenManager.create(storage=storage)
|
|
337
|
+
else:
|
|
338
|
+
# Use default storage
|
|
339
|
+
manager = await CodexTokenManager.create()
|
|
340
|
+
|
|
341
|
+
return await manager.save_credentials(credentials)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error(
|
|
344
|
+
"Failed to save OpenAI credentials",
|
|
345
|
+
error=str(e),
|
|
346
|
+
exc_info=e,
|
|
347
|
+
has_custom_path=bool(custom_path),
|
|
348
|
+
)
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
async def load_credentials(self, custom_path: Any | None = None) -> Any | None:
|
|
352
|
+
"""Load credentials from provider's storage.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
custom_path: Optional custom storage path (Path object)
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Credentials if found, None otherwise
|
|
359
|
+
"""
|
|
360
|
+
from pathlib import Path
|
|
361
|
+
|
|
362
|
+
from ccproxy.auth.storage.generic import GenericJsonStorage
|
|
363
|
+
|
|
364
|
+
from .manager import CodexTokenManager
|
|
365
|
+
from .models import OpenAICredentials
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
if custom_path:
|
|
369
|
+
# Load from custom path
|
|
370
|
+
storage = GenericJsonStorage(Path(custom_path), OpenAICredentials)
|
|
371
|
+
manager = await CodexTokenManager.create(storage=storage)
|
|
372
|
+
else:
|
|
373
|
+
# Load from default storage
|
|
374
|
+
manager = await CodexTokenManager.create()
|
|
375
|
+
|
|
376
|
+
credentials = await manager.load_credentials()
|
|
377
|
+
|
|
378
|
+
# Use standardized profile logging
|
|
379
|
+
self._log_credentials_loaded("codex", credentials)
|
|
380
|
+
|
|
381
|
+
return credentials
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.error(
|
|
384
|
+
"Failed to load OpenAI credentials",
|
|
385
|
+
error=str(e),
|
|
386
|
+
exc_info=e,
|
|
387
|
+
has_custom_path=bool(custom_path),
|
|
388
|
+
)
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
async def create_token_manager(self, storage: Any | None = None) -> Any:
|
|
392
|
+
"""Create and return the token manager instance.
|
|
393
|
+
|
|
394
|
+
Provided to allow core/CLI code to obtain a manager without
|
|
395
|
+
importing plugin classes directly.
|
|
396
|
+
"""
|
|
397
|
+
from .manager import CodexTokenManager
|
|
398
|
+
|
|
399
|
+
return await CodexTokenManager.create(storage=storage)
|
|
400
|
+
|
|
401
|
+
def _extract_standard_profile(
|
|
402
|
+
self, credentials: OpenAICredentials
|
|
403
|
+
) -> StandardProfileFields:
|
|
404
|
+
"""Extract standardized profile fields from OpenAI credentials for UI display.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
credentials: OpenAI credentials with JWT tokens
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
StandardProfileFields with clean, UI-friendly data
|
|
411
|
+
"""
|
|
412
|
+
# Initialize with basic credential info
|
|
413
|
+
from typing import Any
|
|
414
|
+
|
|
415
|
+
profile_data: dict[str, Any] = {
|
|
416
|
+
"account_id": credentials.account_id,
|
|
417
|
+
"provider_type": "codex",
|
|
418
|
+
"active": credentials.active,
|
|
419
|
+
"expired": credentials.is_expired(),
|
|
420
|
+
"has_refresh_token": bool(credentials.refresh_token),
|
|
421
|
+
"has_id_token": bool(credentials.id_token),
|
|
422
|
+
"token_expires_at": credentials.expires_at,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# Store raw credential data for debugging
|
|
426
|
+
raw_data: dict[str, Any] = {
|
|
427
|
+
"last_refresh": credentials.last_refresh,
|
|
428
|
+
"expires_at": str(credentials.expires_at),
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# Extract information from ID token
|
|
432
|
+
if credentials.id_token:
|
|
433
|
+
try:
|
|
434
|
+
import jwt
|
|
435
|
+
|
|
436
|
+
id_claims = jwt.decode(
|
|
437
|
+
credentials.id_token, options={"verify_signature": False}
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Extract UI-friendly profile info
|
|
441
|
+
profile_data.update(
|
|
442
|
+
{
|
|
443
|
+
"email": id_claims.get("email"),
|
|
444
|
+
"email_verified": id_claims.get("email_verified"),
|
|
445
|
+
"display_name": id_claims.get("name")
|
|
446
|
+
or id_claims.get("given_name"),
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Extract subscription information
|
|
451
|
+
auth_claims = id_claims.get("https://api.openai.com/auth", {})
|
|
452
|
+
if isinstance(auth_claims, dict):
|
|
453
|
+
plan_type = auth_claims.get(
|
|
454
|
+
"chatgpt_plan_type"
|
|
455
|
+
) # 'plus', 'pro', etc.
|
|
456
|
+
profile_data.update(
|
|
457
|
+
{
|
|
458
|
+
"subscription_type": plan_type,
|
|
459
|
+
"subscription_status": "active" if plan_type else None,
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Parse subscription dates
|
|
464
|
+
if auth_claims.get("chatgpt_subscription_active_until"):
|
|
465
|
+
try:
|
|
466
|
+
from datetime import datetime
|
|
467
|
+
|
|
468
|
+
expires_str = auth_claims[
|
|
469
|
+
"chatgpt_subscription_active_until"
|
|
470
|
+
]
|
|
471
|
+
profile_data["subscription_expires_at"] = (
|
|
472
|
+
datetime.fromisoformat(
|
|
473
|
+
expires_str.replace("+00:00", "")
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
except Exception:
|
|
477
|
+
pass
|
|
478
|
+
|
|
479
|
+
# Extract organization info
|
|
480
|
+
orgs = auth_claims.get("organizations", [])
|
|
481
|
+
if orgs:
|
|
482
|
+
primary_org = orgs[0] if isinstance(orgs, list) else {}
|
|
483
|
+
if isinstance(primary_org, dict):
|
|
484
|
+
profile_data.update(
|
|
485
|
+
{
|
|
486
|
+
"organization_name": primary_org.get("title"),
|
|
487
|
+
"organization_role": primary_org.get("role"),
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Store full claims for debugging
|
|
492
|
+
raw_data["id_token_claims"] = id_claims
|
|
493
|
+
|
|
494
|
+
except Exception as e:
|
|
495
|
+
logger.debug(
|
|
496
|
+
"Failed to decode ID token for profile extraction", error=str(e)
|
|
497
|
+
)
|
|
498
|
+
raw_data["id_token_decode_error"] = str(e)
|
|
499
|
+
|
|
500
|
+
# Extract access token information
|
|
501
|
+
if credentials.access_token:
|
|
502
|
+
try:
|
|
503
|
+
import jwt
|
|
504
|
+
|
|
505
|
+
access_claims = jwt.decode(
|
|
506
|
+
credentials.access_token, options={"verify_signature": False}
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Store access token info in raw data
|
|
510
|
+
raw_data["access_token_claims"] = {
|
|
511
|
+
"scopes": access_claims.get("scp", []),
|
|
512
|
+
"client_id": access_claims.get("client_id"),
|
|
513
|
+
"audience": access_claims.get("aud"),
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
except Exception as e:
|
|
517
|
+
logger.debug(
|
|
518
|
+
"Failed to decode access token for profile extraction", error=str(e)
|
|
519
|
+
)
|
|
520
|
+
raw_data["access_token_decode_error"] = str(e)
|
|
521
|
+
|
|
522
|
+
# Add provider-specific features
|
|
523
|
+
if profile_data.get("subscription_type"):
|
|
524
|
+
profile_data["features"] = {
|
|
525
|
+
"chatgpt_plus": profile_data["subscription_type"] == "plus",
|
|
526
|
+
"has_subscription": True,
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
profile_data["raw_profile_data"] = raw_data
|
|
530
|
+
|
|
531
|
+
return StandardProfileFields(**profile_data)
|
|
532
|
+
|
|
533
|
+
async def exchange_manual_code(self, code: str) -> Any:
|
|
534
|
+
"""Exchange manual authorization code for tokens.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
code: Authorization code from manual entry
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
OpenAI credentials object
|
|
541
|
+
"""
|
|
542
|
+
# For manual code flow, use OOB redirect URI and no state validation
|
|
543
|
+
credentials: OpenAICredentials = await self.client.handle_callback(
|
|
544
|
+
code, "manual", ""
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if self.storage:
|
|
548
|
+
await self.storage.save(credentials)
|
|
549
|
+
|
|
550
|
+
logger.info(
|
|
551
|
+
"codex_oauth_manual_code_exchanged",
|
|
552
|
+
has_credentials=bool(credentials),
|
|
553
|
+
category="auth",
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
return credentials
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def cli(self) -> CliAuthConfig:
|
|
560
|
+
"""Get CLI authentication configuration for this provider."""
|
|
561
|
+
return CliAuthConfig(
|
|
562
|
+
preferred_flow=FlowType.browser,
|
|
563
|
+
callback_port=1455,
|
|
564
|
+
callback_path="/auth/callback",
|
|
565
|
+
supports_manual_code=True,
|
|
566
|
+
supports_device_flow=False,
|
|
567
|
+
fixed_redirect_uri=None,
|
|
568
|
+
manual_redirect_uri="https://platform.openai.com/oauth/callback",
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
async def cleanup(self) -> None:
|
|
572
|
+
"""Cleanup resources."""
|
|
573
|
+
if self.client:
|
|
574
|
+
await self.client.close()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Token storage for Codex OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ccproxy.auth.storage.base import BaseJsonStorage
|
|
6
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
7
|
+
|
|
8
|
+
from .models import OpenAICredentials
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = get_plugin_logger()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CodexTokenStorage(BaseJsonStorage[OpenAICredentials]):
|
|
15
|
+
"""Codex/OpenAI OAuth-specific token storage implementation."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, storage_path: Path | None = None):
|
|
18
|
+
"""Initialize Codex token storage.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
storage_path: Path to storage file
|
|
22
|
+
"""
|
|
23
|
+
if storage_path is None:
|
|
24
|
+
# Default to standard OpenAI credentials location
|
|
25
|
+
storage_path = Path.home() / ".codex" / "auth.json"
|
|
26
|
+
|
|
27
|
+
super().__init__(storage_path)
|
|
28
|
+
self.provider_name = "codex"
|
|
29
|
+
|
|
30
|
+
async def save(self, credentials: OpenAICredentials) -> bool:
|
|
31
|
+
"""Save OpenAI credentials.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
credentials: OpenAI credentials to save
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if saved successfully, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
# Convert to dict for storage
|
|
41
|
+
data = credentials.model_dump(mode="json", exclude_none=True)
|
|
42
|
+
|
|
43
|
+
# Use parent class's atomic write with backup
|
|
44
|
+
await self._write_json(data)
|
|
45
|
+
|
|
46
|
+
logger.info(
|
|
47
|
+
"codex_oauth_credentials_saved",
|
|
48
|
+
has_refresh_token=bool(credentials.refresh_token),
|
|
49
|
+
storage_path=str(self.file_path),
|
|
50
|
+
category="auth",
|
|
51
|
+
)
|
|
52
|
+
return True
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(
|
|
55
|
+
"codex_oauth_save_failed", error=str(e), exc_info=e, category="auth"
|
|
56
|
+
)
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
async def load(self) -> OpenAICredentials | None:
|
|
60
|
+
"""Load OpenAI credentials.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Stored credentials or None
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
# Use parent class's read method (avoid redundant exists() checks)
|
|
67
|
+
data = await self._read_json()
|
|
68
|
+
if not data:
|
|
69
|
+
logger.debug(
|
|
70
|
+
"codex_auth_file_empty",
|
|
71
|
+
storage_path=str(self.file_path),
|
|
72
|
+
category="auth",
|
|
73
|
+
)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
credentials = OpenAICredentials.model_validate(data)
|
|
77
|
+
logger.info(
|
|
78
|
+
"codex_oauth_credentials_loaded",
|
|
79
|
+
has_refresh_token=bool(credentials.refresh_token),
|
|
80
|
+
category="auth",
|
|
81
|
+
)
|
|
82
|
+
return credentials
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(
|
|
85
|
+
"codex_oauth_credentials_load_error",
|
|
86
|
+
error=str(e),
|
|
87
|
+
exc_info=e,
|
|
88
|
+
category="auth",
|
|
89
|
+
)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# The exists(), delete(), and get_location() methods are inherited from BaseJsonStorage
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Permissions Plugin
|
|
2
|
+
|
|
3
|
+
Provides interactive approval flows for tool calls and other privileged actions.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
- Starts the permission service that tracks and resolves pending requests
|
|
7
|
+
- Exposes SSE and MCP routes for UI, terminal, or IDE integrations
|
|
8
|
+
- Supports configurable timeouts and optional terminal UI prompts
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
- `PermissionsConfig` toggles enablement, stream support, and timeouts
|
|
12
|
+
- Pending requests are handled only when the plugin is enabled
|
|
13
|
+
- Generate defaults with `python3 scripts/generate_config_from_model.py \
|
|
14
|
+
--format toml --plugin permissions --config-class PermissionsConfig`
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[plugins.permissions]
|
|
18
|
+
# enabled = true
|
|
19
|
+
# timeout_seconds = 30
|
|
20
|
+
# enable_terminal_ui = true
|
|
21
|
+
# enable_sse_stream = true
|
|
22
|
+
# cleanup_after_minutes = 5
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Related Components
|
|
26
|
+
- `service.py`: permission service entrypoint
|
|
27
|
+
- `routes.py`: FastAPI router for SSE streaming
|
|
28
|
+
- `mcp/`: MCP server routes used by Claude Code
|