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,256 @@
|
|
|
1
|
+
"""OpenAI/Codex token manager implementation for the Codex plugin."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from ccproxy.auth.exceptions import OAuthTokenRefreshError
|
|
6
|
+
from ccproxy.auth.managers.base import BaseTokenManager
|
|
7
|
+
from ccproxy.auth.managers.token_snapshot import TokenSnapshot
|
|
8
|
+
from ccproxy.auth.storage.base import TokenStorage
|
|
9
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
10
|
+
|
|
11
|
+
from .models import OpenAICredentials, OpenAIProfileInfo, OpenAITokenWrapper
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = get_plugin_logger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CodexTokenManager(BaseTokenManager[OpenAICredentials]):
|
|
18
|
+
"""Manager for Codex/OpenAI token storage and operations.
|
|
19
|
+
|
|
20
|
+
Uses the generic storage and wrapper pattern for consistency.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
storage: TokenStorage[OpenAICredentials] | None = None,
|
|
26
|
+
):
|
|
27
|
+
"""Initialize Codex token manager.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
storage: Optional custom storage, defaults to standard location
|
|
31
|
+
"""
|
|
32
|
+
if storage is None:
|
|
33
|
+
# Use the Codex-specific storage for ~/.codex/auth.json
|
|
34
|
+
from .storage import CodexTokenStorage
|
|
35
|
+
|
|
36
|
+
storage = CodexTokenStorage()
|
|
37
|
+
super().__init__(storage)
|
|
38
|
+
self._profile_cache: OpenAIProfileInfo | None = None
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
async def create(
|
|
42
|
+
cls, storage: TokenStorage[OpenAICredentials] | None = None
|
|
43
|
+
) -> "CodexTokenManager":
|
|
44
|
+
"""Async factory for parity with other managers.
|
|
45
|
+
|
|
46
|
+
Codex/OpenAI does not need to preload remote data, but this keeps a
|
|
47
|
+
consistent async creation API across managers.
|
|
48
|
+
"""
|
|
49
|
+
return cls(storage=storage)
|
|
50
|
+
|
|
51
|
+
def _build_token_snapshot(self, credentials: OpenAICredentials) -> TokenSnapshot:
|
|
52
|
+
"""Construct a snapshot for OpenAI credentials."""
|
|
53
|
+
wrapper = OpenAITokenWrapper(credentials=credentials)
|
|
54
|
+
extras = {
|
|
55
|
+
"id_token_present": bool(wrapper.id_token),
|
|
56
|
+
}
|
|
57
|
+
return TokenSnapshot(
|
|
58
|
+
provider="codex",
|
|
59
|
+
account_id=wrapper.account_id,
|
|
60
|
+
access_token=str(wrapper.access_token_value),
|
|
61
|
+
refresh_token=wrapper.refresh_token_value,
|
|
62
|
+
expires_at=wrapper.expires_at_datetime,
|
|
63
|
+
extras=extras,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# ==================== Abstract Method Implementations ====================
|
|
67
|
+
|
|
68
|
+
async def refresh_token(self) -> OpenAICredentials | None:
|
|
69
|
+
"""Refresh the access token using the refresh token.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Updated credentials or None if refresh failed
|
|
73
|
+
"""
|
|
74
|
+
# Load current credentials
|
|
75
|
+
credentials = await self.load_credentials()
|
|
76
|
+
if not credentials:
|
|
77
|
+
logger.error("no_credentials_to_refresh", category="auth")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
if not credentials.refresh_token:
|
|
81
|
+
logger.error("no_refresh_token_available", category="auth")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Refresh directly using a local OAuth client/provider (no global registry)
|
|
86
|
+
from .provider import CodexOAuthProvider
|
|
87
|
+
|
|
88
|
+
provider = CodexOAuthProvider()
|
|
89
|
+
new_credentials: OpenAICredentials = await provider.refresh_access_token(
|
|
90
|
+
credentials.refresh_token
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Preserve account_id if not in new credentials
|
|
94
|
+
if not new_credentials.account_id and credentials.account_id:
|
|
95
|
+
# Preserve via nested tokens structure
|
|
96
|
+
new_credentials.tokens.account_id = credentials.account_id
|
|
97
|
+
|
|
98
|
+
# Save updated credentials
|
|
99
|
+
if await self.save_credentials(new_credentials):
|
|
100
|
+
logger.info(
|
|
101
|
+
"Token refreshed successfully",
|
|
102
|
+
account_id=new_credentials.account_id,
|
|
103
|
+
category="auth",
|
|
104
|
+
)
|
|
105
|
+
# Clear profile cache as token changed
|
|
106
|
+
self._profile_cache = None
|
|
107
|
+
return new_credentials
|
|
108
|
+
|
|
109
|
+
logger.error("failed_to_save_refreshed_credentials", category="auth")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(
|
|
114
|
+
"Token refresh failed",
|
|
115
|
+
error=str(e),
|
|
116
|
+
exc_info=False,
|
|
117
|
+
category="auth",
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def is_expired(self, credentials: OpenAICredentials) -> bool:
|
|
122
|
+
"""Check if credentials are expired using wrapper."""
|
|
123
|
+
if isinstance(credentials, OpenAICredentials):
|
|
124
|
+
wrapper = OpenAITokenWrapper(credentials=credentials)
|
|
125
|
+
return bool(wrapper.is_expired)
|
|
126
|
+
|
|
127
|
+
expires_at = getattr(credentials, "expires_at", None)
|
|
128
|
+
if not expires_at:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
if isinstance(expires_at, datetime):
|
|
132
|
+
return expires_at <= datetime.now(UTC)
|
|
133
|
+
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def get_account_id(self, credentials: OpenAICredentials) -> str | None:
|
|
137
|
+
"""Get account ID from credentials."""
|
|
138
|
+
return credentials.account_id
|
|
139
|
+
|
|
140
|
+
def get_expiration_time(self, credentials: OpenAICredentials) -> datetime | None:
|
|
141
|
+
"""Get expiration time as datetime."""
|
|
142
|
+
return credentials.expires_at
|
|
143
|
+
|
|
144
|
+
# ==================== OpenAI-Specific Methods ====================
|
|
145
|
+
|
|
146
|
+
async def get_profile_quick(self) -> OpenAIProfileInfo | None:
|
|
147
|
+
"""Lightweight profile from cached data or JWT claims.
|
|
148
|
+
|
|
149
|
+
Avoids any remote calls. Uses cache if populated, otherwise derives
|
|
150
|
+
directly from stored credentials' JWT claims.
|
|
151
|
+
"""
|
|
152
|
+
if self._profile_cache:
|
|
153
|
+
return self._profile_cache
|
|
154
|
+
|
|
155
|
+
credentials = await self.load_credentials()
|
|
156
|
+
if not credentials or self.is_expired(credentials):
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
self._profile_cache = OpenAIProfileInfo.from_token(credentials)
|
|
160
|
+
return self._profile_cache
|
|
161
|
+
|
|
162
|
+
async def get_profile(self) -> OpenAIProfileInfo | None:
|
|
163
|
+
"""Get user profile from JWT token.
|
|
164
|
+
|
|
165
|
+
OpenAI doesn't provide a profile API, so we extract
|
|
166
|
+
all information from the JWT token claims.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
OpenAIProfileInfo or None if not authenticated
|
|
170
|
+
"""
|
|
171
|
+
if self._profile_cache:
|
|
172
|
+
return self._profile_cache
|
|
173
|
+
|
|
174
|
+
credentials = await self.load_credentials()
|
|
175
|
+
if not credentials or self.is_expired(credentials):
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Extract profile from JWT token claims
|
|
179
|
+
self._profile_cache = OpenAIProfileInfo.from_token(credentials)
|
|
180
|
+
return self._profile_cache
|
|
181
|
+
|
|
182
|
+
async def get_access_token_with_refresh(self) -> str | None:
|
|
183
|
+
"""Get valid access token, automatically refreshing if expired.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Access token if available and valid, None otherwise
|
|
187
|
+
"""
|
|
188
|
+
credentials = await self.load_credentials()
|
|
189
|
+
if not credentials:
|
|
190
|
+
logger.debug("no_credentials_found", category="auth")
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
needs_refresh = self.should_refresh(credentials)
|
|
194
|
+
|
|
195
|
+
if needs_refresh:
|
|
196
|
+
logger.info(
|
|
197
|
+
"openai_token_refresh_needed",
|
|
198
|
+
reason="expired" if self.is_expired(credentials) else "expiring_soon",
|
|
199
|
+
expires_in=self.seconds_until_expiration(credentials),
|
|
200
|
+
category="auth",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if credentials.refresh_token:
|
|
204
|
+
try:
|
|
205
|
+
refreshed = await self.refresh_token()
|
|
206
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
207
|
+
logger.warning(
|
|
208
|
+
"openai_token_refresh_exception",
|
|
209
|
+
error=str(exc),
|
|
210
|
+
category="auth",
|
|
211
|
+
)
|
|
212
|
+
raise OAuthTokenRefreshError("OpenAI token refresh failed") from exc
|
|
213
|
+
|
|
214
|
+
if refreshed:
|
|
215
|
+
logger.info("OpenAI token refreshed successfully", category="auth")
|
|
216
|
+
credentials = refreshed
|
|
217
|
+
else:
|
|
218
|
+
logger.warning("openai_token_refresh_failed", category="auth")
|
|
219
|
+
raise OAuthTokenRefreshError("OpenAI token refresh failed")
|
|
220
|
+
else:
|
|
221
|
+
logger.warning(
|
|
222
|
+
"Cannot refresh OpenAI token - no refresh token available",
|
|
223
|
+
category="auth",
|
|
224
|
+
)
|
|
225
|
+
raise OAuthTokenRefreshError("OpenAI token refresh failed")
|
|
226
|
+
|
|
227
|
+
return credentials.access_token
|
|
228
|
+
|
|
229
|
+
async def get_access_token(self) -> str | None:
|
|
230
|
+
"""Override base method to return token even if expired.
|
|
231
|
+
|
|
232
|
+
Will attempt refresh if expired but still returns the token
|
|
233
|
+
even if refresh fails, letting the API handle authorization.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Access token if available (expired or not), None only if no credentials
|
|
237
|
+
"""
|
|
238
|
+
credentials = await self.load_credentials()
|
|
239
|
+
if not credentials:
|
|
240
|
+
logger.debug("no_credentials_found", category="auth")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# Check if token is expired
|
|
244
|
+
needs_refresh = self.should_refresh(credentials)
|
|
245
|
+
|
|
246
|
+
if needs_refresh:
|
|
247
|
+
try:
|
|
248
|
+
return await self.get_access_token_with_refresh()
|
|
249
|
+
except OAuthTokenRefreshError as exc:
|
|
250
|
+
logger.warning(
|
|
251
|
+
"OpenAI token refresh failed, using existing token",
|
|
252
|
+
error=str(exc),
|
|
253
|
+
category="auth",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return credentials.access_token
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""OpenAI-specific authentication models."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
import jwt
|
|
7
|
+
from pydantic import (
|
|
8
|
+
BaseModel,
|
|
9
|
+
Field,
|
|
10
|
+
SecretStr,
|
|
11
|
+
computed_field,
|
|
12
|
+
field_serializer,
|
|
13
|
+
field_validator,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ccproxy.auth.models.base import BaseProfileInfo, BaseTokenInfo
|
|
17
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = get_plugin_logger()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OpenAITokens(BaseModel):
|
|
24
|
+
"""Nested token structure from OpenAI OAuth."""
|
|
25
|
+
|
|
26
|
+
id_token: SecretStr = Field(..., description="OpenAI ID token (JWT)")
|
|
27
|
+
access_token: SecretStr = Field(..., description="OpenAI access token (JWT)")
|
|
28
|
+
refresh_token: SecretStr = Field(..., description="OpenAI refresh token")
|
|
29
|
+
account_id: str = Field(..., description="OpenAI account ID")
|
|
30
|
+
|
|
31
|
+
@field_serializer("id_token", "access_token", "refresh_token")
|
|
32
|
+
def serialize_secret(self, value: SecretStr) -> str:
|
|
33
|
+
"""Serialize SecretStr to plain string for JSON output."""
|
|
34
|
+
return value.get_secret_value() if value else ""
|
|
35
|
+
|
|
36
|
+
@field_validator("id_token", "access_token", "refresh_token", mode="before")
|
|
37
|
+
@classmethod
|
|
38
|
+
def validate_tokens(cls, v: str | SecretStr | None) -> SecretStr | None:
|
|
39
|
+
"""Convert string values to SecretStr."""
|
|
40
|
+
if v is None:
|
|
41
|
+
return None
|
|
42
|
+
if isinstance(v, str):
|
|
43
|
+
return SecretStr(v)
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OpenAICredentials(BaseModel):
|
|
48
|
+
"""OpenAI authentication credentials model matching actual auth file schema."""
|
|
49
|
+
|
|
50
|
+
OPENAI_API_KEY: str | None = Field(
|
|
51
|
+
None, description="Legacy API key (usually null)"
|
|
52
|
+
)
|
|
53
|
+
tokens: OpenAITokens = Field(..., description="OAuth token information")
|
|
54
|
+
last_refresh: str = Field(..., description="Last refresh timestamp as ISO string")
|
|
55
|
+
active: bool = Field(default=True, description="Whether credentials are active")
|
|
56
|
+
# No legacy compatibility shims; callers must provide nested `tokens` structure
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def access_token(self) -> str:
|
|
60
|
+
"""Get access token from nested structure."""
|
|
61
|
+
return self.tokens.access_token.get_secret_value()
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def refresh_token(self) -> str:
|
|
65
|
+
"""Get refresh token from nested structure."""
|
|
66
|
+
return self.tokens.refresh_token.get_secret_value()
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def id_token(self) -> str:
|
|
70
|
+
"""Get ID token from nested structure."""
|
|
71
|
+
return self.tokens.id_token.get_secret_value()
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def account_id(self) -> str:
|
|
75
|
+
"""Get account ID from nested structure."""
|
|
76
|
+
return self.tokens.account_id
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def expires_at(self) -> datetime:
|
|
80
|
+
"""Extract expiration from access token JWT."""
|
|
81
|
+
try:
|
|
82
|
+
# Decode JWT without verification to extract 'exp' claim
|
|
83
|
+
decoded = jwt.decode(
|
|
84
|
+
self.tokens.access_token.get_secret_value(),
|
|
85
|
+
options={"verify_signature": False},
|
|
86
|
+
)
|
|
87
|
+
exp_timestamp = decoded.get("exp")
|
|
88
|
+
if exp_timestamp:
|
|
89
|
+
return datetime.fromtimestamp(exp_timestamp, tz=UTC)
|
|
90
|
+
except (jwt.DecodeError, jwt.InvalidTokenError, KeyError, ValueError) as e:
|
|
91
|
+
logger.debug("Failed to extract expiration from access token", error=str(e))
|
|
92
|
+
|
|
93
|
+
# Fallback to a reasonable default if we can't decode
|
|
94
|
+
return datetime.now(UTC).replace(hour=23, minute=59, second=59)
|
|
95
|
+
|
|
96
|
+
def is_expired(self) -> bool:
|
|
97
|
+
"""Check if the access token is expired."""
|
|
98
|
+
now = datetime.now(UTC)
|
|
99
|
+
return now >= self.expires_at
|
|
100
|
+
|
|
101
|
+
def expires_in_seconds(self) -> int:
|
|
102
|
+
"""Get seconds until token expires."""
|
|
103
|
+
now = datetime.now(UTC)
|
|
104
|
+
delta = self.expires_at - now
|
|
105
|
+
return max(0, int(delta.total_seconds()))
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict[str, Any]:
|
|
108
|
+
"""Convert to dictionary for storage.
|
|
109
|
+
|
|
110
|
+
Implements BaseCredentials protocol.
|
|
111
|
+
"""
|
|
112
|
+
return {
|
|
113
|
+
"OPENAI_API_KEY": self.OPENAI_API_KEY,
|
|
114
|
+
"tokens": {
|
|
115
|
+
"id_token": self.tokens.id_token.get_secret_value(),
|
|
116
|
+
"access_token": self.tokens.access_token.get_secret_value(),
|
|
117
|
+
"refresh_token": self.tokens.refresh_token.get_secret_value(),
|
|
118
|
+
"account_id": self.tokens.account_id,
|
|
119
|
+
},
|
|
120
|
+
"last_refresh": self.last_refresh,
|
|
121
|
+
"active": self.active,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_dict(cls, data: dict[str, Any]) -> "OpenAICredentials":
|
|
126
|
+
"""Create from dictionary.
|
|
127
|
+
|
|
128
|
+
Implements BaseCredentials protocol.
|
|
129
|
+
"""
|
|
130
|
+
return cls(**data)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class OpenAITokenWrapper(BaseTokenInfo):
|
|
134
|
+
"""Wrapper for OpenAI credentials that adds computed properties.
|
|
135
|
+
|
|
136
|
+
This wrapper maintains the original OpenAICredentials structure
|
|
137
|
+
while providing a unified interface through BaseTokenInfo.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
# Embed the original credentials to preserve JSON schema
|
|
141
|
+
credentials: OpenAICredentials
|
|
142
|
+
|
|
143
|
+
@computed_field
|
|
144
|
+
def access_token_value(self) -> str:
|
|
145
|
+
"""Get access token (now SecretStr in OpenAI)."""
|
|
146
|
+
return self.credentials.access_token
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def refresh_token_value(self) -> str | None:
|
|
150
|
+
"""Get refresh token."""
|
|
151
|
+
return self.credentials.refresh_token
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def expires_at_datetime(self) -> datetime:
|
|
155
|
+
"""Get expiration (already a datetime in OpenAI)."""
|
|
156
|
+
return self.credentials.expires_at
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def account_id(self) -> str:
|
|
160
|
+
"""Get account ID (extracted from JWT by validator)."""
|
|
161
|
+
return self.credentials.account_id
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def id_token(self) -> str | None:
|
|
165
|
+
"""Get ID token if available."""
|
|
166
|
+
return self.credentials.id_token
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class OpenAIProfileInfo(BaseProfileInfo):
|
|
170
|
+
"""OpenAI-specific profile extracted from JWT tokens.
|
|
171
|
+
|
|
172
|
+
OpenAI embeds profile information in JWT claims rather
|
|
173
|
+
than providing a separate API endpoint.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
provider_type: Literal["openai"] = "openai"
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_token(cls, credentials: OpenAICredentials) -> "OpenAIProfileInfo":
|
|
180
|
+
"""Extract profile from JWT token claims.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
credentials: OpenAI credentials containing JWT tokens
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
OpenAIProfileInfo with all JWT claims preserved
|
|
187
|
+
"""
|
|
188
|
+
# Prefer id_token as it has more claims, fallback to access_token
|
|
189
|
+
token_to_decode = credentials.id_token or credentials.access_token
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
# Decode without verification to extract claims
|
|
193
|
+
claims = jwt.decode(token_to_decode, options={"verify_signature": False})
|
|
194
|
+
logger.debug(
|
|
195
|
+
"Extracted JWT claims", num_claims=len(claims), category="auth"
|
|
196
|
+
)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.warning("failed_to_decode_jwt_token", error=str(e), category="auth")
|
|
199
|
+
claims = {}
|
|
200
|
+
|
|
201
|
+
# Use the account_id already extracted by OpenAICredentials validator
|
|
202
|
+
account_id = credentials.account_id
|
|
203
|
+
|
|
204
|
+
# Extract common fields if present in claims
|
|
205
|
+
email = claims.get("email", "")
|
|
206
|
+
display_name = claims.get("name") or claims.get("given_name")
|
|
207
|
+
|
|
208
|
+
# Store ALL JWT claims in extras for complete information
|
|
209
|
+
# This includes: sub, aud, iss, exp, iat, org_id, chatgpt_account_id, etc.
|
|
210
|
+
return cls(
|
|
211
|
+
account_id=account_id,
|
|
212
|
+
email=email,
|
|
213
|
+
display_name=display_name,
|
|
214
|
+
extras=claims, # Preserve all JWT claims
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def chatgpt_account_id(self) -> str | None:
|
|
219
|
+
"""Get ChatGPT account ID from JWT claims."""
|
|
220
|
+
auth_claims = self.extras.get("https://api.openai.com/auth", {})
|
|
221
|
+
if isinstance(auth_claims, dict):
|
|
222
|
+
return auth_claims.get("chatgpt_account_id")
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def organization_id(self) -> str | None:
|
|
227
|
+
"""Get organization ID from JWT claims."""
|
|
228
|
+
# Check in auth claims first
|
|
229
|
+
auth_claims = self.extras.get("https://api.openai.com/auth", {})
|
|
230
|
+
if isinstance(auth_claims, dict) and "organization_id" in auth_claims:
|
|
231
|
+
return str(auth_claims["organization_id"])
|
|
232
|
+
# Fallback to top-level org_id
|
|
233
|
+
org_id = self.extras.get("org_id")
|
|
234
|
+
return str(org_id) if org_id is not None else None
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def auth0_subject(self) -> str | None:
|
|
238
|
+
"""Get Auth0 subject (sub claim)."""
|
|
239
|
+
return self.extras.get("sub")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""OAuth Codex plugin v2 implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from ccproxy.auth.oauth import OAuthProviderProtocol
|
|
6
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
7
|
+
from ccproxy.core.plugins import (
|
|
8
|
+
AuthProviderPluginFactory,
|
|
9
|
+
AuthProviderPluginRuntime,
|
|
10
|
+
PluginContext,
|
|
11
|
+
PluginManifest,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .config import CodexOAuthConfig
|
|
15
|
+
from .provider import CodexOAuthProvider
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = get_plugin_logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OAuthCodexRuntime(AuthProviderPluginRuntime):
|
|
22
|
+
"""Runtime for OAuth Codex plugin."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, manifest: PluginManifest):
|
|
25
|
+
"""Initialize runtime."""
|
|
26
|
+
super().__init__(manifest)
|
|
27
|
+
self.config: CodexOAuthConfig | None = None
|
|
28
|
+
|
|
29
|
+
async def _on_initialize(self) -> None:
|
|
30
|
+
"""Initialize the OAuth Codex plugin."""
|
|
31
|
+
logger.debug(
|
|
32
|
+
"oauth_codex_initializing",
|
|
33
|
+
context_keys=list(self.context.keys()) if self.context else [],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Get configuration
|
|
37
|
+
if self.context:
|
|
38
|
+
config = self.context.get("config")
|
|
39
|
+
if not isinstance(config, CodexOAuthConfig):
|
|
40
|
+
# Use default config if none provided
|
|
41
|
+
config = CodexOAuthConfig()
|
|
42
|
+
logger.debug("oauth_codex_using_default_config")
|
|
43
|
+
self.config = config
|
|
44
|
+
|
|
45
|
+
# Call parent initialization which handles provider registration
|
|
46
|
+
await super()._on_initialize()
|
|
47
|
+
|
|
48
|
+
logger.debug(
|
|
49
|
+
"oauth_codex_plugin_initialized",
|
|
50
|
+
status="initialized",
|
|
51
|
+
provider_name=self.auth_provider.provider_name
|
|
52
|
+
if self.auth_provider
|
|
53
|
+
else "unknown",
|
|
54
|
+
category="plugin",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class OAuthCodexFactory(AuthProviderPluginFactory):
|
|
59
|
+
"""Factory for OAuth Codex plugin."""
|
|
60
|
+
|
|
61
|
+
cli_safe = True # Safe for CLI - provides auth only
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
"""Initialize factory with manifest."""
|
|
65
|
+
# Create manifest with static declarations
|
|
66
|
+
manifest = PluginManifest(
|
|
67
|
+
name="oauth_codex",
|
|
68
|
+
version="0.1.0",
|
|
69
|
+
description="Standalone OpenAI Codex OAuth authentication provider plugin",
|
|
70
|
+
is_provider=True, # It's a provider plugin but focused on OAuth
|
|
71
|
+
config_class=CodexOAuthConfig,
|
|
72
|
+
dependencies=[],
|
|
73
|
+
routes=[], # No HTTP routes needed
|
|
74
|
+
tasks=[], # No scheduled tasks needed
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Initialize with manifest
|
|
78
|
+
super().__init__(manifest)
|
|
79
|
+
|
|
80
|
+
def create_context(self, core_services: Any) -> PluginContext:
|
|
81
|
+
"""Create context with auth provider components.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
core_services: Core services container
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Plugin context with auth provider components
|
|
88
|
+
"""
|
|
89
|
+
# Start with base context
|
|
90
|
+
context = super().create_context(core_services)
|
|
91
|
+
|
|
92
|
+
# Create auth provider for this plugin
|
|
93
|
+
auth_provider = self.create_auth_provider(context)
|
|
94
|
+
context["auth_provider"] = auth_provider
|
|
95
|
+
|
|
96
|
+
# Add other auth-specific components if needed
|
|
97
|
+
storage = self.create_storage()
|
|
98
|
+
if storage:
|
|
99
|
+
context["storage"] = storage
|
|
100
|
+
|
|
101
|
+
return context
|
|
102
|
+
|
|
103
|
+
def create_runtime(self) -> OAuthCodexRuntime:
|
|
104
|
+
"""Create runtime instance."""
|
|
105
|
+
return OAuthCodexRuntime(self.manifest)
|
|
106
|
+
|
|
107
|
+
def create_auth_provider(
|
|
108
|
+
self, context: PluginContext | None = None
|
|
109
|
+
) -> OAuthProviderProtocol:
|
|
110
|
+
"""Create OAuth provider instance.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
context: Optional plugin context containing http_client
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
CodexOAuthProvider instance
|
|
117
|
+
"""
|
|
118
|
+
# Prefer validated config from context when available
|
|
119
|
+
if context and isinstance(context.get("config"), CodexOAuthConfig):
|
|
120
|
+
cfg = cast(CodexOAuthConfig, context.get("config"))
|
|
121
|
+
else:
|
|
122
|
+
cfg = CodexOAuthConfig()
|
|
123
|
+
config: CodexOAuthConfig = cfg
|
|
124
|
+
http_client = context.get("http_client") if context else None
|
|
125
|
+
hook_manager = context.get("hook_manager") if context else None
|
|
126
|
+
settings = context.get("settings") if context else None
|
|
127
|
+
provider = CodexOAuthProvider(
|
|
128
|
+
config,
|
|
129
|
+
http_client=http_client,
|
|
130
|
+
hook_manager=hook_manager,
|
|
131
|
+
settings=settings,
|
|
132
|
+
)
|
|
133
|
+
return cast(OAuthProviderProtocol, provider)
|
|
134
|
+
|
|
135
|
+
def create_storage(self) -> Any | None:
|
|
136
|
+
"""Create storage for OAuth credentials.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Storage instance or None to use provider's default
|
|
140
|
+
"""
|
|
141
|
+
# CodexOAuthProvider manages its own storage internally
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Export the factory instance
|
|
146
|
+
factory = OAuthCodexFactory()
|