ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Token storage for Claude OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from ccproxy.auth.storage.base import BaseJsonStorage
|
|
10
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
11
|
+
|
|
12
|
+
from .models import ClaudeCredentials, ClaudeProfileInfo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = get_plugin_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClaudeOAuthStorage(BaseJsonStorage[ClaudeCredentials]):
|
|
19
|
+
"""Claude OAuth-specific token storage implementation."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, storage_path: Path | None = None):
|
|
22
|
+
"""Initialize Claude OAuth token storage.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
storage_path: Path to storage file
|
|
26
|
+
"""
|
|
27
|
+
if storage_path is None:
|
|
28
|
+
# Default to standard Claude credentials location
|
|
29
|
+
storage_path = Path.home() / ".claude" / ".credentials.json"
|
|
30
|
+
|
|
31
|
+
super().__init__(storage_path)
|
|
32
|
+
self.provider_name = "claude-api"
|
|
33
|
+
|
|
34
|
+
async def save(self, credentials: ClaudeCredentials) -> bool:
|
|
35
|
+
"""Save Claude credentials.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
credentials: Claude credentials to save
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if saved successfully, False otherwise
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
# Convert to dict for storage (uses by_alias=True by default)
|
|
45
|
+
data = credentials.model_dump(mode="json", exclude_none=True)
|
|
46
|
+
|
|
47
|
+
# Use parent class's atomic write with backup
|
|
48
|
+
await self._write_json(data)
|
|
49
|
+
|
|
50
|
+
logger.debug(
|
|
51
|
+
"claude_oauth_credentials_saved",
|
|
52
|
+
has_oauth=bool(credentials.claude_ai_oauth),
|
|
53
|
+
storage_path=str(self.file_path),
|
|
54
|
+
category="auth",
|
|
55
|
+
)
|
|
56
|
+
return True
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.error(
|
|
59
|
+
"claude_oauth_save_failed", error=str(e), exc_info=e, category="auth"
|
|
60
|
+
)
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
async def load(self) -> ClaudeCredentials | None:
|
|
64
|
+
"""Load Claude credentials.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Stored credentials or None
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
# Use parent class's read method
|
|
71
|
+
data = await self._read_json()
|
|
72
|
+
if not data:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
credentials = ClaudeCredentials.model_validate(data)
|
|
76
|
+
logger.debug(
|
|
77
|
+
"claude_oauth_credentials_loaded",
|
|
78
|
+
has_oauth=bool(credentials.claude_ai_oauth),
|
|
79
|
+
category="auth",
|
|
80
|
+
)
|
|
81
|
+
return credentials
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(
|
|
84
|
+
"claude_oauth_credentials_load_error",
|
|
85
|
+
error=str(e),
|
|
86
|
+
exc_info=e,
|
|
87
|
+
category="auth",
|
|
88
|
+
)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# The exists(), delete(), and get_location() methods are inherited from BaseJsonStorage
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ClaudeProfileStorage:
|
|
95
|
+
"""Claude profile storage implementation for .account.json."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, storage_path: Path | None = None):
|
|
98
|
+
"""Initialize Claude profile storage.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
storage_path: Path to storage file
|
|
102
|
+
"""
|
|
103
|
+
if storage_path is None:
|
|
104
|
+
# Default to standard Claude account location
|
|
105
|
+
storage_path = Path.home() / ".claude" / ".account.json"
|
|
106
|
+
|
|
107
|
+
self.file_path = storage_path
|
|
108
|
+
|
|
109
|
+
async def _write_json(self, data: dict[str, Any]) -> None:
|
|
110
|
+
"""Write JSON data to file atomically.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
data: JSON data to write
|
|
114
|
+
"""
|
|
115
|
+
# Ensure parent directory exists
|
|
116
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
# Write to temp file first for atomic operation
|
|
119
|
+
def write_file() -> None:
|
|
120
|
+
with tempfile.NamedTemporaryFile(
|
|
121
|
+
mode="w",
|
|
122
|
+
dir=self.file_path.parent,
|
|
123
|
+
delete=False,
|
|
124
|
+
prefix=".tmp_",
|
|
125
|
+
suffix=".json",
|
|
126
|
+
) as tmp_file:
|
|
127
|
+
json.dump(data, tmp_file, indent=2)
|
|
128
|
+
tmp_path = Path(tmp_file.name)
|
|
129
|
+
|
|
130
|
+
# Set proper permissions before moving
|
|
131
|
+
tmp_path.chmod(0o600)
|
|
132
|
+
# Atomic rename
|
|
133
|
+
tmp_path.replace(self.file_path)
|
|
134
|
+
|
|
135
|
+
await asyncio.to_thread(write_file)
|
|
136
|
+
|
|
137
|
+
async def _read_json(self) -> dict[str, Any] | None:
|
|
138
|
+
"""Read JSON data from file.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Parsed JSON data or None if file doesn't exist
|
|
142
|
+
"""
|
|
143
|
+
if not self.file_path.exists():
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def read_file() -> dict[str, Any]:
|
|
147
|
+
with self.file_path.open("r") as f:
|
|
148
|
+
return cast(dict[str, Any], json.load(f))
|
|
149
|
+
|
|
150
|
+
return cast(dict[str, Any], await asyncio.to_thread(read_file))
|
|
151
|
+
|
|
152
|
+
async def save_profile(self, profile_data: dict[str, Any]) -> bool:
|
|
153
|
+
"""Save Claude profile data.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
profile_data: Raw profile data from API
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
True if saved successfully, False otherwise
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
# Write the raw profile data
|
|
163
|
+
await self._write_json(profile_data)
|
|
164
|
+
|
|
165
|
+
# Extract key info for logging
|
|
166
|
+
account = profile_data.get("account", {})
|
|
167
|
+
logger.info(
|
|
168
|
+
"claude_profile_saved",
|
|
169
|
+
account_id=account.get("uuid"),
|
|
170
|
+
email=account.get("email"),
|
|
171
|
+
has_claude_pro=account.get("has_claude_pro"),
|
|
172
|
+
has_claude_max=account.get("has_claude_max"),
|
|
173
|
+
storage_path=str(self.file_path),
|
|
174
|
+
category="auth",
|
|
175
|
+
)
|
|
176
|
+
return True
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(
|
|
179
|
+
"claude_profile_save_failed",
|
|
180
|
+
error=str(e),
|
|
181
|
+
exc_info=e,
|
|
182
|
+
category="auth",
|
|
183
|
+
)
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
async def load_profile(self) -> ClaudeProfileInfo | None:
|
|
187
|
+
"""Load Claude profile.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
ClaudeProfileInfo or None if not found
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
data = await self._read_json()
|
|
194
|
+
if not data:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
profile = ClaudeProfileInfo.from_api_response(data)
|
|
198
|
+
logger.debug(
|
|
199
|
+
"claude_profile_loaded",
|
|
200
|
+
account_id=profile.account_id,
|
|
201
|
+
email=profile.email,
|
|
202
|
+
category="auth",
|
|
203
|
+
)
|
|
204
|
+
return profile
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(
|
|
207
|
+
"claude_profile_load_error",
|
|
208
|
+
error=str(e),
|
|
209
|
+
exc_info=e,
|
|
210
|
+
category="auth",
|
|
211
|
+
)
|
|
212
|
+
return None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# OAuth Codex Plugin
|
|
2
|
+
|
|
3
|
+
OAuth-only plugin that issues and refreshes tokens for the Codex provider.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
- Provides `CodexOAuthProvider` implementing the OAuth dance for Codex APIs
|
|
7
|
+
- Registers an auth manager consumable by `codex` and related plugins
|
|
8
|
+
- Leverages shared HTTP client and hook infrastructure from the container
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
- `CodexOAuthConfig` configures client ID, secrets, scopes, and cache paths
|
|
12
|
+
- Enable when using Codex endpoints that require OAuth tokens
|
|
13
|
+
- Generate defaults with `python3 scripts/generate_config_from_model.py \
|
|
14
|
+
--format toml --plugin oauth_codex --config-class CodexOAuthConfig`
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[plugins.oauth_codex]
|
|
18
|
+
# enabled = true
|
|
19
|
+
# base_url = "https://auth.openai.com"
|
|
20
|
+
# token_url = "https://auth.openai.com/oauth/token"
|
|
21
|
+
# authorize_url = "https://auth.openai.com/oauth/authorize"
|
|
22
|
+
# profile_url = "https://api.openai.com/oauth/profile"
|
|
23
|
+
# client_id = "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
24
|
+
# redirect_uri = "http://localhost:1455/auth/callback"
|
|
25
|
+
# scopes = ["openid", "profile", "email", "offline_access"]
|
|
26
|
+
# headers = {"User-Agent" = "Codex-Code/1.0.43"}
|
|
27
|
+
# audience = "https://api.openai.com/v1"
|
|
28
|
+
# user_agent = "Codex-Code/1.0.43"
|
|
29
|
+
# request_timeout = 30
|
|
30
|
+
# callback_timeout = 300
|
|
31
|
+
# callback_port = 1455
|
|
32
|
+
# use_pkce = true
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Related Components
|
|
36
|
+
- `provider.py`: OAuth implementation for Codex credentials
|
|
37
|
+
- `plugin.py`: runtime lifecycle for the auth plugin
|
|
38
|
+
- `config.py`: settings model for OAuth parameters
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""OAuth Codex plugin for standalone OpenAI Codex OAuth authentication."""
|
|
2
|
+
|
|
3
|
+
from .client import CodexOAuthClient
|
|
4
|
+
from .config import CodexOAuthConfig
|
|
5
|
+
from .provider import CodexOAuthProvider
|
|
6
|
+
from .storage import CodexTokenStorage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CodexOAuthClient",
|
|
11
|
+
"CodexOAuthConfig",
|
|
12
|
+
"CodexOAuthProvider",
|
|
13
|
+
"CodexTokenStorage",
|
|
14
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Codex/OpenAI OAuth client implementation."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import jwt
|
|
8
|
+
from pydantic import SecretStr
|
|
9
|
+
|
|
10
|
+
from ccproxy.auth.exceptions import OAuthError
|
|
11
|
+
from ccproxy.auth.oauth.base import BaseOAuthClient
|
|
12
|
+
from ccproxy.auth.storage.base import TokenStorage
|
|
13
|
+
from ccproxy.config.settings import Settings
|
|
14
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
15
|
+
|
|
16
|
+
from .config import CodexOAuthConfig
|
|
17
|
+
from .models import OpenAICredentials, OpenAITokens
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = get_plugin_logger()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CodexOAuthClient(BaseOAuthClient[OpenAICredentials]):
|
|
24
|
+
"""Codex/OpenAI OAuth implementation for the OAuth Codex plugin."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
config: CodexOAuthConfig,
|
|
29
|
+
storage: TokenStorage[OpenAICredentials] | None = None,
|
|
30
|
+
http_client: httpx.AsyncClient | None = None,
|
|
31
|
+
hook_manager: Any | None = None,
|
|
32
|
+
settings: Settings | None = None,
|
|
33
|
+
):
|
|
34
|
+
"""Initialize Codex OAuth client.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config: OAuth configuration
|
|
38
|
+
storage: Token storage backend
|
|
39
|
+
http_client: Optional HTTP client (for request tracing support)
|
|
40
|
+
hook_manager: Optional hook manager for emitting events
|
|
41
|
+
settings: Optional settings for HTTP client configuration
|
|
42
|
+
"""
|
|
43
|
+
self.oauth_config = config
|
|
44
|
+
|
|
45
|
+
# Resolve effective redirect URI from config
|
|
46
|
+
redirect_uri = config.get_redirect_uri()
|
|
47
|
+
|
|
48
|
+
# Initialize base class
|
|
49
|
+
super().__init__(
|
|
50
|
+
client_id=config.client_id,
|
|
51
|
+
redirect_uri=redirect_uri,
|
|
52
|
+
base_url=config.base_url,
|
|
53
|
+
scopes=config.scopes,
|
|
54
|
+
storage=storage,
|
|
55
|
+
http_client=http_client,
|
|
56
|
+
hook_manager=hook_manager,
|
|
57
|
+
settings=settings,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _get_auth_endpoint(self) -> str:
|
|
61
|
+
"""Get OpenAI OAuth authorization endpoint.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Full authorization endpoint URL
|
|
65
|
+
"""
|
|
66
|
+
return self.oauth_config.authorize_url
|
|
67
|
+
|
|
68
|
+
def _get_token_endpoint(self) -> str:
|
|
69
|
+
"""Get OpenAI OAuth token exchange endpoint.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Full token endpoint URL
|
|
73
|
+
"""
|
|
74
|
+
return self.oauth_config.token_url
|
|
75
|
+
|
|
76
|
+
def get_custom_auth_params(self) -> dict[str, str]:
|
|
77
|
+
"""Get OpenAI-specific authorization parameters.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dictionary of custom parameters
|
|
81
|
+
"""
|
|
82
|
+
# OpenAI does not use the audience parameter in authorization requests
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
def get_custom_headers(self) -> dict[str, str]:
|
|
86
|
+
"""Get OpenAI-specific HTTP headers.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dictionary of custom headers
|
|
90
|
+
"""
|
|
91
|
+
return {
|
|
92
|
+
"User-Agent": self.oauth_config.user_agent,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async def parse_token_response(self, data: dict[str, Any]) -> OpenAICredentials:
|
|
96
|
+
"""Parse OpenAI-specific token response.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
data: Raw token response from OpenAI
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
OpenAI credentials object
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
OAuthError: If response parsing fails
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
# Extract tokens
|
|
109
|
+
access_token: str = data["access_token"]
|
|
110
|
+
refresh_token: str = data.get("refresh_token", "")
|
|
111
|
+
id_token: str = data.get("id_token", "")
|
|
112
|
+
|
|
113
|
+
# Build credentials in the current nested schema; legacy inputs are also accepted
|
|
114
|
+
# by the model's validator if needed.
|
|
115
|
+
tokens = OpenAITokens(
|
|
116
|
+
id_token=SecretStr(id_token),
|
|
117
|
+
access_token=SecretStr(access_token),
|
|
118
|
+
refresh_token=SecretStr(refresh_token or ""),
|
|
119
|
+
account_id="",
|
|
120
|
+
)
|
|
121
|
+
credentials = OpenAICredentials(
|
|
122
|
+
OPENAI_API_KEY=None,
|
|
123
|
+
tokens=tokens,
|
|
124
|
+
last_refresh=datetime.now(UTC).replace(microsecond=0).isoformat(),
|
|
125
|
+
active=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Try to extract account_id from JWT claims (id_token preferred)
|
|
129
|
+
try:
|
|
130
|
+
token_to_decode = id_token or access_token
|
|
131
|
+
decoded = jwt.decode(
|
|
132
|
+
token_to_decode, options={"verify_signature": False}
|
|
133
|
+
)
|
|
134
|
+
account_id = (
|
|
135
|
+
decoded.get("sub")
|
|
136
|
+
or decoded.get("account_id")
|
|
137
|
+
or decoded.get("org_id")
|
|
138
|
+
or ""
|
|
139
|
+
)
|
|
140
|
+
# Pydantic model has properties mapping; update underlying field
|
|
141
|
+
credentials.tokens.account_id = str(account_id)
|
|
142
|
+
logger.debug(
|
|
143
|
+
"codex_oauth_id_token_decoded",
|
|
144
|
+
sub=decoded.get("sub"),
|
|
145
|
+
email=decoded.get("email"),
|
|
146
|
+
category="auth",
|
|
147
|
+
)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.warning(
|
|
150
|
+
"codex_oauth_id_token_decode_error",
|
|
151
|
+
error=str(e),
|
|
152
|
+
exc_info=e,
|
|
153
|
+
category="auth",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
logger.info(
|
|
157
|
+
"codex_oauth_credentials_parsed",
|
|
158
|
+
has_refresh_token=bool(refresh_token),
|
|
159
|
+
has_id_token=bool(id_token),
|
|
160
|
+
account_id=credentials.account_id,
|
|
161
|
+
category="auth",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return credentials
|
|
165
|
+
|
|
166
|
+
except KeyError as e:
|
|
167
|
+
logger.error(
|
|
168
|
+
"codex_oauth_token_response_missing_field",
|
|
169
|
+
missing_field=str(e),
|
|
170
|
+
response_keys=list(data.keys()),
|
|
171
|
+
category="auth",
|
|
172
|
+
)
|
|
173
|
+
raise OAuthError(f"Missing required field in token response: {e}") from e
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(
|
|
176
|
+
"codex_oauth_token_response_parse_error",
|
|
177
|
+
error=str(e),
|
|
178
|
+
error_type=type(e).__name__,
|
|
179
|
+
category="auth",
|
|
180
|
+
)
|
|
181
|
+
raise OAuthError(f"Failed to parse OpenAI token response: {e}") from e
|
|
182
|
+
|
|
183
|
+
async def refresh_token(self, refresh_token: str) -> OpenAICredentials:
|
|
184
|
+
"""Refresh OpenAI access token.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
refresh_token: Refresh token
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
New OpenAI credentials
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
OAuthError: If refresh fails
|
|
194
|
+
"""
|
|
195
|
+
token_endpoint = self._get_token_endpoint()
|
|
196
|
+
data = {
|
|
197
|
+
"grant_type": "refresh_token",
|
|
198
|
+
"refresh_token": refresh_token,
|
|
199
|
+
"client_id": self.client_id,
|
|
200
|
+
"scope": "openid profile email offline_access",
|
|
201
|
+
}
|
|
202
|
+
headers = self.get_custom_headers()
|
|
203
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
response = await self.http_client.post(
|
|
207
|
+
token_endpoint,
|
|
208
|
+
data=data, # OpenAI uses form encoding
|
|
209
|
+
headers=headers,
|
|
210
|
+
timeout=30.0,
|
|
211
|
+
)
|
|
212
|
+
response.raise_for_status()
|
|
213
|
+
|
|
214
|
+
token_response = response.json()
|
|
215
|
+
return await self.parse_token_response(token_response)
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(
|
|
219
|
+
"codex_oauth_token_refresh_failed",
|
|
220
|
+
error=str(e),
|
|
221
|
+
exc_info=False,
|
|
222
|
+
category="auth",
|
|
223
|
+
)
|
|
224
|
+
raise OAuthError(f"Failed to refresh OpenAI token: {e}") from e
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""OpenAI Codex-specific configuration settings."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CodexOAuthConfig(BaseModel):
|
|
7
|
+
"""OAuth-specific configuration for OpenAI Codex."""
|
|
8
|
+
|
|
9
|
+
enabled: bool = Field(
|
|
10
|
+
default=True,
|
|
11
|
+
description="Enable the plugin",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Core OAuth endpoints and identifiers (aligns with Claude config structure)
|
|
15
|
+
base_url: str = Field(
|
|
16
|
+
default="https://auth.openai.com",
|
|
17
|
+
description="Base URL for OAuth API endpoints",
|
|
18
|
+
)
|
|
19
|
+
token_url: str = Field(
|
|
20
|
+
default="https://auth.openai.com/oauth/token",
|
|
21
|
+
description="OAuth token endpoint URL",
|
|
22
|
+
)
|
|
23
|
+
authorize_url: str = Field(
|
|
24
|
+
default="https://auth.openai.com/oauth/authorize",
|
|
25
|
+
description="OAuth authorization endpoint URL",
|
|
26
|
+
)
|
|
27
|
+
profile_url: str = Field(
|
|
28
|
+
default="https://api.openai.com/oauth/profile",
|
|
29
|
+
description="OAuth profile endpoint URL",
|
|
30
|
+
)
|
|
31
|
+
client_id: str = Field(
|
|
32
|
+
default="app_EMoamEEZ73f0CkXaXp7hrann",
|
|
33
|
+
description="OpenAI OAuth client ID",
|
|
34
|
+
)
|
|
35
|
+
redirect_uri: str | None = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
description="OAuth redirect URI (auto-generated from callback_port if not set)",
|
|
38
|
+
)
|
|
39
|
+
scopes: list[str] = Field(
|
|
40
|
+
default_factory=lambda: [
|
|
41
|
+
"openid",
|
|
42
|
+
"profile",
|
|
43
|
+
"email",
|
|
44
|
+
"offline_access",
|
|
45
|
+
],
|
|
46
|
+
description="OAuth scopes to request",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Additional request configuration (mirrors Claude config shape)
|
|
50
|
+
headers: dict[str, str] = Field(
|
|
51
|
+
default_factory=lambda: {
|
|
52
|
+
"User-Agent": "Codex-Code/1.0.43", # Match default user agent in config
|
|
53
|
+
},
|
|
54
|
+
description="Additional headers for OAuth requests",
|
|
55
|
+
)
|
|
56
|
+
# Optional audience parameter for auth requests (OpenAI specific)
|
|
57
|
+
audience: str = Field(
|
|
58
|
+
default="https://api.openai.com/v1",
|
|
59
|
+
description="OAuth audience parameter for OpenAI",
|
|
60
|
+
)
|
|
61
|
+
# Convenience user agent string (mirrors headers[\"User-Agent\"]) for typed access
|
|
62
|
+
user_agent: str = Field(
|
|
63
|
+
default="Codex-Code/1.0.43",
|
|
64
|
+
description="User-Agent header value for OAuth requests",
|
|
65
|
+
)
|
|
66
|
+
request_timeout: int = Field(
|
|
67
|
+
default=30,
|
|
68
|
+
description="Timeout in seconds for OAuth requests",
|
|
69
|
+
)
|
|
70
|
+
callback_timeout: int = Field(
|
|
71
|
+
default=300,
|
|
72
|
+
description="Timeout in seconds for OAuth callback",
|
|
73
|
+
ge=60,
|
|
74
|
+
le=600,
|
|
75
|
+
)
|
|
76
|
+
callback_port: int = Field(
|
|
77
|
+
default=1455,
|
|
78
|
+
description="Port for OAuth callback server",
|
|
79
|
+
ge=1024,
|
|
80
|
+
le=65535,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def get_redirect_uri(self) -> str:
|
|
84
|
+
"""Return redirect URI, auto-generated from callback_port when unset.
|
|
85
|
+
|
|
86
|
+
Uses the standard plugin callback path: `/auth/callback`.
|
|
87
|
+
"""
|
|
88
|
+
if self.redirect_uri:
|
|
89
|
+
return self.redirect_uri
|
|
90
|
+
return f"http://localhost:{self.callback_port}/auth/callback"
|
|
91
|
+
|
|
92
|
+
use_pkce: bool = Field(
|
|
93
|
+
default=True,
|
|
94
|
+
description="Whether to use PKCE flow (OpenAI requires it)",
|
|
95
|
+
)
|