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,263 @@
|
|
|
1
|
+
"""Codex plugin local CLI health models and detection models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Annotated, Any, Literal, TypedDict
|
|
8
|
+
|
|
9
|
+
from pydantic import (
|
|
10
|
+
BaseModel,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
Field,
|
|
13
|
+
field_serializer,
|
|
14
|
+
field_validator,
|
|
15
|
+
model_validator,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ccproxy.llms.models import anthropic as anthropic_models
|
|
19
|
+
from ccproxy.models.detection import DetectedHeaders, DetectedPrompts
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CodexCliStatus(str, Enum):
|
|
23
|
+
AVAILABLE = "available"
|
|
24
|
+
NOT_INSTALLED = "not_installed"
|
|
25
|
+
BINARY_FOUND_BUT_ERRORS = "binary_found_but_errors"
|
|
26
|
+
TIMEOUT = "timeout"
|
|
27
|
+
ERROR = "error"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CodexCliInfo(BaseModel):
|
|
31
|
+
status: CodexCliStatus
|
|
32
|
+
version: str | None = None
|
|
33
|
+
binary_path: str | None = None
|
|
34
|
+
version_output: str | None = None
|
|
35
|
+
error: str | None = None
|
|
36
|
+
return_code: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CodexHeaders(BaseModel):
|
|
40
|
+
"""Pydantic model for Codex CLI headers extraction with field aliases."""
|
|
41
|
+
|
|
42
|
+
session_id: str = Field(
|
|
43
|
+
alias="session_id",
|
|
44
|
+
description="Codex session identifier",
|
|
45
|
+
default="",
|
|
46
|
+
)
|
|
47
|
+
originator: str = Field(
|
|
48
|
+
description="Codex originator identifier",
|
|
49
|
+
default="codex_cli_rs",
|
|
50
|
+
)
|
|
51
|
+
openai_beta: str = Field(
|
|
52
|
+
alias="openai-beta",
|
|
53
|
+
description="OpenAI beta features",
|
|
54
|
+
default="responses=experimental",
|
|
55
|
+
)
|
|
56
|
+
version: str = Field(
|
|
57
|
+
description="Codex CLI version",
|
|
58
|
+
default="0.21.0",
|
|
59
|
+
)
|
|
60
|
+
chatgpt_account_id: str = Field(
|
|
61
|
+
alias="chatgpt-account-id",
|
|
62
|
+
description="ChatGPT account identifier",
|
|
63
|
+
default="",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
|
67
|
+
|
|
68
|
+
def to_headers_dict(self) -> dict[str, str]:
|
|
69
|
+
"""Convert to headers dictionary for HTTP forwarding with proper case."""
|
|
70
|
+
headers = {}
|
|
71
|
+
|
|
72
|
+
# Map field names to proper HTTP header names
|
|
73
|
+
header_mapping = {
|
|
74
|
+
"session_id": "session_id",
|
|
75
|
+
"originator": "originator",
|
|
76
|
+
"openai_beta": "openai-beta",
|
|
77
|
+
"version": "version",
|
|
78
|
+
"chatgpt_account_id": "chatgpt-account-id",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for field_name, header_name in header_mapping.items():
|
|
82
|
+
value = getattr(self, field_name, None)
|
|
83
|
+
if value is not None and value != "":
|
|
84
|
+
headers[header_name] = value
|
|
85
|
+
|
|
86
|
+
return headers
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CodexInstructionsData(BaseModel):
|
|
90
|
+
"""Extracted Codex instructions information."""
|
|
91
|
+
|
|
92
|
+
instructions_field: Annotated[
|
|
93
|
+
str,
|
|
94
|
+
Field(
|
|
95
|
+
description="Complete instructions field as detected from Codex CLI, preserving exact text content"
|
|
96
|
+
),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
model_config = ConfigDict(extra="allow")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CodexCacheData(BaseModel):
|
|
103
|
+
"""Cached Codex CLI detection data with version tracking."""
|
|
104
|
+
|
|
105
|
+
codex_version: Annotated[str, Field(description="Codex CLI version")]
|
|
106
|
+
headers: Annotated[
|
|
107
|
+
DetectedHeaders,
|
|
108
|
+
Field(
|
|
109
|
+
description="Captured headers (lowercase keys) in insertion order",
|
|
110
|
+
default_factory=DetectedHeaders,
|
|
111
|
+
),
|
|
112
|
+
]
|
|
113
|
+
prompts: Annotated[
|
|
114
|
+
DetectedPrompts,
|
|
115
|
+
Field(description="Captured prompt metadata", default_factory=DetectedPrompts),
|
|
116
|
+
]
|
|
117
|
+
body_json: Annotated[
|
|
118
|
+
dict[str, Any] | None,
|
|
119
|
+
Field(
|
|
120
|
+
description="Legacy captured request body (deprecated)",
|
|
121
|
+
default=None,
|
|
122
|
+
exclude=True,
|
|
123
|
+
),
|
|
124
|
+
] = None
|
|
125
|
+
method: Annotated[
|
|
126
|
+
str | None, Field(description="Captured HTTP method", default=None)
|
|
127
|
+
] = None
|
|
128
|
+
url: Annotated[str | None, Field(description="Captured full URL", default=None)] = (
|
|
129
|
+
None
|
|
130
|
+
)
|
|
131
|
+
path: Annotated[
|
|
132
|
+
str | None, Field(description="Captured request path", default=None)
|
|
133
|
+
] = None
|
|
134
|
+
query_params: Annotated[
|
|
135
|
+
dict[str, str] | None,
|
|
136
|
+
Field(description="Captured query parameters", default=None),
|
|
137
|
+
] = None
|
|
138
|
+
cached_at: datetime = Field(
|
|
139
|
+
description="Cache timestamp",
|
|
140
|
+
default_factory=lambda: datetime.now(UTC),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
model_config = ConfigDict(extra="forbid")
|
|
144
|
+
|
|
145
|
+
@model_validator(mode="before")
|
|
146
|
+
@classmethod
|
|
147
|
+
def _coerce_legacy_format(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
148
|
+
if not isinstance(values, dict):
|
|
149
|
+
return values
|
|
150
|
+
|
|
151
|
+
if "prompts" not in values:
|
|
152
|
+
legacy_body = values.get("body_json")
|
|
153
|
+
if legacy_body is not None:
|
|
154
|
+
values["prompts"] = DetectedPrompts.from_body(legacy_body)
|
|
155
|
+
cls._log_legacy_usage("body_json")
|
|
156
|
+
|
|
157
|
+
return values
|
|
158
|
+
|
|
159
|
+
@field_validator("headers", mode="before")
|
|
160
|
+
@classmethod
|
|
161
|
+
def _validate_headers(cls, value: Any) -> DetectedHeaders:
|
|
162
|
+
if isinstance(value, DetectedHeaders):
|
|
163
|
+
return value
|
|
164
|
+
if isinstance(value, dict):
|
|
165
|
+
return DetectedHeaders(value)
|
|
166
|
+
if value is None:
|
|
167
|
+
cls._log_legacy_usage("missing_headers")
|
|
168
|
+
return DetectedHeaders()
|
|
169
|
+
raise TypeError("headers must be a mapping of strings")
|
|
170
|
+
|
|
171
|
+
@field_validator("prompts", mode="before")
|
|
172
|
+
@classmethod
|
|
173
|
+
def _validate_prompts(cls, value: Any) -> DetectedPrompts:
|
|
174
|
+
if isinstance(value, DetectedPrompts):
|
|
175
|
+
return value
|
|
176
|
+
if isinstance(value, dict):
|
|
177
|
+
return DetectedPrompts.from_body(value)
|
|
178
|
+
if value is None:
|
|
179
|
+
return DetectedPrompts()
|
|
180
|
+
raise TypeError("prompts must be derived from a mapping")
|
|
181
|
+
|
|
182
|
+
@field_serializer("headers")
|
|
183
|
+
def _serialize_headers(self, headers: DetectedHeaders) -> dict[str, str]:
|
|
184
|
+
return headers.as_dict()
|
|
185
|
+
|
|
186
|
+
@field_serializer("prompts")
|
|
187
|
+
def _serialize_prompts(self, prompts: DetectedPrompts) -> dict[str, Any]:
|
|
188
|
+
raw = prompts.raw or {}
|
|
189
|
+
if not isinstance(raw, dict):
|
|
190
|
+
raw = {}
|
|
191
|
+
if prompts.instructions and "instructions" not in raw:
|
|
192
|
+
raw = dict(raw)
|
|
193
|
+
raw["instructions"] = prompts.instructions
|
|
194
|
+
if prompts.system is not None and "system" not in raw:
|
|
195
|
+
raw = dict(raw)
|
|
196
|
+
raw["system"] = prompts.system
|
|
197
|
+
return raw
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _log_legacy_usage(reason: str) -> None:
|
|
201
|
+
try:
|
|
202
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
203
|
+
|
|
204
|
+
logger = get_plugin_logger()
|
|
205
|
+
logger.debug(
|
|
206
|
+
"legacy_detection_cache_format",
|
|
207
|
+
plugin="codex",
|
|
208
|
+
reason=reason,
|
|
209
|
+
)
|
|
210
|
+
except Exception: # pragma: no cover - logging best-effort only
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class CodexMessage(BaseModel):
|
|
215
|
+
"""Message format for Codex requests."""
|
|
216
|
+
|
|
217
|
+
role: Annotated[Literal["user", "assistant"], Field(description="Message role")]
|
|
218
|
+
content: Annotated[str, Field(description="Message content")]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class CodexRequest(BaseModel):
|
|
222
|
+
"""OpenAI Codex completion request model."""
|
|
223
|
+
|
|
224
|
+
model: Annotated[str, Field(description="Model name (e.g., gpt-5)")] = "gpt-5"
|
|
225
|
+
instructions: Annotated[
|
|
226
|
+
str | None, Field(description="System instructions for the model")
|
|
227
|
+
] = None
|
|
228
|
+
messages: Annotated[list[CodexMessage], Field(description="Conversation messages")]
|
|
229
|
+
stream: Annotated[bool, Field(description="Whether to stream the response")] = True
|
|
230
|
+
|
|
231
|
+
model_config = ConfigDict(
|
|
232
|
+
extra="allow"
|
|
233
|
+
) # Allow additional fields for compatibility
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class CodexResponse(BaseModel):
|
|
237
|
+
"""OpenAI Codex completion response model."""
|
|
238
|
+
|
|
239
|
+
id: Annotated[str, Field(description="Response ID")]
|
|
240
|
+
model: Annotated[str, Field(description="Model used for completion")]
|
|
241
|
+
content: Annotated[str, Field(description="Generated content")]
|
|
242
|
+
finish_reason: Annotated[
|
|
243
|
+
str | None, Field(description="Reason the response finished")
|
|
244
|
+
] = None
|
|
245
|
+
usage: Annotated[
|
|
246
|
+
anthropic_models.Usage | None, Field(description="Token usage information")
|
|
247
|
+
] = None
|
|
248
|
+
|
|
249
|
+
model_config = ConfigDict(
|
|
250
|
+
extra="allow"
|
|
251
|
+
) # Allow additional fields for compatibility
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class CodexAuthData(TypedDict, total=False):
|
|
255
|
+
"""Authentication data for Codex/OpenAI provider.
|
|
256
|
+
|
|
257
|
+
Attributes:
|
|
258
|
+
access_token: Bearer token for OpenAI API authentication
|
|
259
|
+
chatgpt_account_id: Account ID for ChatGPT session-based requests
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
access_token: str | None
|
|
263
|
+
chatgpt_account_id: str | None
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Codex provider plugin v2 implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from ccproxy.core.constants import (
|
|
6
|
+
FORMAT_OPENAI_CHAT,
|
|
7
|
+
FORMAT_OPENAI_RESPONSES,
|
|
8
|
+
)
|
|
9
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
10
|
+
from ccproxy.core.plugins import (
|
|
11
|
+
BaseProviderPluginFactory,
|
|
12
|
+
FormatAdapterSpec,
|
|
13
|
+
FormatPair,
|
|
14
|
+
PluginContext,
|
|
15
|
+
PluginManifest,
|
|
16
|
+
ProviderPluginRuntime,
|
|
17
|
+
)
|
|
18
|
+
from ccproxy.core.plugins.declaration import RouterSpec
|
|
19
|
+
from ccproxy.llms.streaming.accumulators import OpenAIAccumulator
|
|
20
|
+
from ccproxy.plugins.oauth_codex.manager import CodexTokenManager
|
|
21
|
+
|
|
22
|
+
from .adapter import CodexAdapter
|
|
23
|
+
from .config import CodexSettings
|
|
24
|
+
from .detection_service import CodexDetectionService
|
|
25
|
+
from .routes import router as codex_router
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
logger = get_plugin_logger()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CodexRuntime(ProviderPluginRuntime):
|
|
36
|
+
"""Runtime for Codex provider plugin."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, manifest: PluginManifest):
|
|
39
|
+
"""Initialize runtime."""
|
|
40
|
+
super().__init__(manifest)
|
|
41
|
+
self.config: CodexSettings | None = None
|
|
42
|
+
self.credential_manager: CodexTokenManager | None = None
|
|
43
|
+
|
|
44
|
+
async def _on_initialize(self) -> None:
|
|
45
|
+
"""Initialize the Codex provider plugin."""
|
|
46
|
+
if not self.context:
|
|
47
|
+
raise RuntimeError("Context not set")
|
|
48
|
+
|
|
49
|
+
# Get configuration
|
|
50
|
+
try:
|
|
51
|
+
config = self.context.get(CodexSettings)
|
|
52
|
+
except ValueError:
|
|
53
|
+
logger.debug("plugin_no_config")
|
|
54
|
+
# Use default config if none provided
|
|
55
|
+
config = CodexSettings()
|
|
56
|
+
logger.debug("plugin_using_default_config")
|
|
57
|
+
self.config = config
|
|
58
|
+
|
|
59
|
+
# Get auth manager from context
|
|
60
|
+
try:
|
|
61
|
+
self.credential_manager = self.context.get(CodexTokenManager)
|
|
62
|
+
except ValueError:
|
|
63
|
+
self.credential_manager = None
|
|
64
|
+
|
|
65
|
+
# Call parent to initialize adapter and detection service
|
|
66
|
+
await super()._on_initialize()
|
|
67
|
+
|
|
68
|
+
# Register streaming metrics hook
|
|
69
|
+
await self._register_streaming_metrics_hook()
|
|
70
|
+
|
|
71
|
+
# Check CLI status
|
|
72
|
+
if self.detection_service:
|
|
73
|
+
version = self.detection_service.get_version()
|
|
74
|
+
cli_path = self.detection_service.get_cli_path()
|
|
75
|
+
|
|
76
|
+
if not cli_path:
|
|
77
|
+
logger.warning(
|
|
78
|
+
"cli_detection_completed",
|
|
79
|
+
cli_available=False,
|
|
80
|
+
version=None,
|
|
81
|
+
cli_path=None,
|
|
82
|
+
source="unknown",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Get CLI info for consolidated logging (only for successful detection)
|
|
86
|
+
cli_info = {}
|
|
87
|
+
if self.detection_service and self.detection_service.get_cli_path():
|
|
88
|
+
cli_info.update(
|
|
89
|
+
{
|
|
90
|
+
"cli_available": True,
|
|
91
|
+
"cli_version": self.detection_service.get_version(),
|
|
92
|
+
"cli_path": self.detection_service.get_cli_path(),
|
|
93
|
+
"cli_source": "package_manager",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
logger.debug(
|
|
98
|
+
"plugin_initialized",
|
|
99
|
+
plugin="codex",
|
|
100
|
+
version=self.manifest.version,
|
|
101
|
+
status="initialized",
|
|
102
|
+
has_credentials=self.credential_manager is not None,
|
|
103
|
+
has_adapter=self.adapter is not None,
|
|
104
|
+
has_detection=self.detection_service is not None,
|
|
105
|
+
**cli_info,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def _get_health_details(self) -> dict[str, Any]:
|
|
109
|
+
"""Get health check details."""
|
|
110
|
+
details = await super()._get_health_details()
|
|
111
|
+
|
|
112
|
+
# Add Codex-specific details
|
|
113
|
+
if self.config:
|
|
114
|
+
details.update(
|
|
115
|
+
{
|
|
116
|
+
"base_url": self.config.base_url,
|
|
117
|
+
"supports_streaming": self.config.supports_streaming,
|
|
118
|
+
"models": [card.id for card in self.config.models_endpoint],
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Add authentication status
|
|
123
|
+
if self.credential_manager:
|
|
124
|
+
try:
|
|
125
|
+
auth_status = await self.credential_manager.get_auth_status()
|
|
126
|
+
details["auth_configured"] = auth_status.get("auth_configured", False)
|
|
127
|
+
details["token_available"] = auth_status.get("token_available", False)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
details["auth_error"] = str(e)
|
|
130
|
+
|
|
131
|
+
# Include standardized provider health check details
|
|
132
|
+
try:
|
|
133
|
+
from .health import codex_health_check
|
|
134
|
+
|
|
135
|
+
if self.config and self.detection_service:
|
|
136
|
+
health_result = await codex_health_check(
|
|
137
|
+
self.config,
|
|
138
|
+
self.detection_service,
|
|
139
|
+
self.credential_manager,
|
|
140
|
+
version=self.manifest.version,
|
|
141
|
+
)
|
|
142
|
+
details.update(
|
|
143
|
+
{
|
|
144
|
+
"health_check_status": health_result.status,
|
|
145
|
+
"health_check_detail": health_result.details,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
details["health_check_error"] = str(e)
|
|
150
|
+
|
|
151
|
+
return details
|
|
152
|
+
|
|
153
|
+
async def _register_streaming_metrics_hook(self) -> None:
|
|
154
|
+
"""Register the streaming metrics extraction hook."""
|
|
155
|
+
try:
|
|
156
|
+
if not self.context:
|
|
157
|
+
logger.warning(
|
|
158
|
+
"streaming_metrics_hook_not_registered",
|
|
159
|
+
reason="no_context",
|
|
160
|
+
plugin="codex",
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
# Get hook registry from context
|
|
164
|
+
from ccproxy.core.plugins.hooks.registry import HookRegistry
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
hook_registry = self.context.get(HookRegistry)
|
|
168
|
+
except ValueError:
|
|
169
|
+
logger.warning(
|
|
170
|
+
"streaming_metrics_hook_not_registered",
|
|
171
|
+
reason="no_hook_registry",
|
|
172
|
+
plugin="codex",
|
|
173
|
+
context_keys=list(self.context.keys()) if self.context else [],
|
|
174
|
+
)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Get pricing service from plugin registry if available
|
|
178
|
+
pricing_service = None
|
|
179
|
+
if "plugin_registry" in self.context:
|
|
180
|
+
try:
|
|
181
|
+
from ccproxy.plugins.pricing.service import PricingService
|
|
182
|
+
|
|
183
|
+
plugin_registry = self.context["plugin_registry"]
|
|
184
|
+
pricing_service = plugin_registry.get_service(
|
|
185
|
+
"pricing", PricingService
|
|
186
|
+
)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.debug(
|
|
189
|
+
"pricing_service_not_available_for_hook",
|
|
190
|
+
plugin="codex",
|
|
191
|
+
error=str(e),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Create and register the hook
|
|
195
|
+
from .hooks import CodexStreamingMetricsHook
|
|
196
|
+
|
|
197
|
+
# Pass both pricing_service (if available now) and plugin_registry (for lazy loading)
|
|
198
|
+
metrics_hook = CodexStreamingMetricsHook(
|
|
199
|
+
pricing_service=pricing_service,
|
|
200
|
+
plugin_registry=self.context.get("plugin_registry"),
|
|
201
|
+
)
|
|
202
|
+
hook_registry.register(metrics_hook)
|
|
203
|
+
|
|
204
|
+
logger.debug(
|
|
205
|
+
"streaming_metrics_hook_registered",
|
|
206
|
+
plugin="codex",
|
|
207
|
+
hook_name=metrics_hook.name,
|
|
208
|
+
priority=metrics_hook.priority,
|
|
209
|
+
has_pricing=pricing_service is not None,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(
|
|
214
|
+
"streaming_metrics_hook_registration_failed",
|
|
215
|
+
plugin="codex",
|
|
216
|
+
error=str(e),
|
|
217
|
+
exc_info=e,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class CodexFactory(BaseProviderPluginFactory):
|
|
222
|
+
"""Factory for Codex provider plugin."""
|
|
223
|
+
|
|
224
|
+
cli_safe = False # Heavy provider plugin - not safe for CLI
|
|
225
|
+
|
|
226
|
+
# Plugin configuration via class attributes
|
|
227
|
+
plugin_name = "codex"
|
|
228
|
+
plugin_description = (
|
|
229
|
+
"OpenAI Codex provider plugin with OAuth authentication and format conversion"
|
|
230
|
+
)
|
|
231
|
+
runtime_class = CodexRuntime
|
|
232
|
+
adapter_class = CodexAdapter
|
|
233
|
+
detection_service_class = CodexDetectionService
|
|
234
|
+
config_class = CodexSettings
|
|
235
|
+
# String-based auth manager reference
|
|
236
|
+
auth_manager_name = "oauth_codex"
|
|
237
|
+
credentials_manager_class = CodexTokenManager
|
|
238
|
+
routers = [
|
|
239
|
+
RouterSpec(router=codex_router, prefix="/codex"),
|
|
240
|
+
]
|
|
241
|
+
dependencies = ["oauth_codex"]
|
|
242
|
+
optional_requires = ["pricing"]
|
|
243
|
+
|
|
244
|
+
# No format adapters needed - core provides all required conversions
|
|
245
|
+
format_adapters: list[FormatAdapterSpec] = []
|
|
246
|
+
|
|
247
|
+
# Define requirements for adapters this plugin needs
|
|
248
|
+
requires_format_adapters: list[FormatPair] = [
|
|
249
|
+
# Codex can leverage core-provided OpenAI chat ↔ responses conversion
|
|
250
|
+
(FORMAT_OPENAI_CHAT, FORMAT_OPENAI_RESPONSES),
|
|
251
|
+
]
|
|
252
|
+
tool_accumulator_class = OpenAIAccumulator
|
|
253
|
+
|
|
254
|
+
def create_detection_service(self, context: PluginContext) -> CodexDetectionService:
|
|
255
|
+
"""Create the Codex detection service with validation."""
|
|
256
|
+
from ccproxy.config.settings import Settings
|
|
257
|
+
from ccproxy.services.cli_detection import CLIDetectionService
|
|
258
|
+
|
|
259
|
+
settings = context.get(Settings)
|
|
260
|
+
try:
|
|
261
|
+
cli_service = context.get(CLIDetectionService)
|
|
262
|
+
except ValueError:
|
|
263
|
+
cli_service = None
|
|
264
|
+
|
|
265
|
+
# Get codex-specific settings
|
|
266
|
+
try:
|
|
267
|
+
codex_settings = context.get(CodexSettings)
|
|
268
|
+
except ValueError:
|
|
269
|
+
codex_settings = None
|
|
270
|
+
|
|
271
|
+
return CodexDetectionService(settings, cli_service, codex_settings)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# Export the factory instance
|
|
275
|
+
factory = CodexFactory()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Codex plugin routes."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, Request
|
|
6
|
+
from starlette.responses import Response, StreamingResponse
|
|
7
|
+
|
|
8
|
+
from ccproxy.api.decorators import with_format_chain
|
|
9
|
+
from ccproxy.api.dependencies import (
|
|
10
|
+
get_plugin_adapter,
|
|
11
|
+
get_provider_config_dependency,
|
|
12
|
+
)
|
|
13
|
+
from ccproxy.auth.dependencies import ConditionalAuthDep
|
|
14
|
+
from ccproxy.core.constants import (
|
|
15
|
+
FORMAT_ANTHROPIC_MESSAGES,
|
|
16
|
+
FORMAT_OPENAI_CHAT,
|
|
17
|
+
FORMAT_OPENAI_RESPONSES,
|
|
18
|
+
UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
|
|
19
|
+
UPSTREAM_ENDPOINT_OPENAI_CHAT_COMPLETIONS,
|
|
20
|
+
UPSTREAM_ENDPOINT_OPENAI_RESPONSES,
|
|
21
|
+
)
|
|
22
|
+
from ccproxy.streaming import DeferredStreaming
|
|
23
|
+
|
|
24
|
+
from .config import CodexSettings
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
CodexAdapterDep = Annotated[Any, Depends(get_plugin_adapter("codex"))]
|
|
31
|
+
CodexConfigDep = Annotated[
|
|
32
|
+
CodexSettings,
|
|
33
|
+
Depends(get_provider_config_dependency("codex", CodexSettings)),
|
|
34
|
+
]
|
|
35
|
+
router = APIRouter()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Helper to handle adapter requests
|
|
39
|
+
async def handle_codex_request(
|
|
40
|
+
request: Request,
|
|
41
|
+
adapter: Any,
|
|
42
|
+
) -> StreamingResponse | Response | DeferredStreaming:
|
|
43
|
+
result = await adapter.handle_request(request)
|
|
44
|
+
return cast(StreamingResponse | Response | DeferredStreaming, result)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Route definitions
|
|
48
|
+
async def _codex_responses_handler(
|
|
49
|
+
request: Request,
|
|
50
|
+
adapter: CodexAdapterDep,
|
|
51
|
+
) -> StreamingResponse | Response | DeferredStreaming:
|
|
52
|
+
"""Shared handler for Codex responses endpoints."""
|
|
53
|
+
|
|
54
|
+
return await handle_codex_request(request, adapter)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@router.post("/v1/responses", response_model=None)
|
|
58
|
+
@with_format_chain(
|
|
59
|
+
[FORMAT_OPENAI_RESPONSES], endpoint=UPSTREAM_ENDPOINT_OPENAI_RESPONSES
|
|
60
|
+
)
|
|
61
|
+
async def codex_responses(
|
|
62
|
+
request: Request,
|
|
63
|
+
auth: ConditionalAuthDep,
|
|
64
|
+
adapter: CodexAdapterDep,
|
|
65
|
+
) -> StreamingResponse | Response | DeferredStreaming:
|
|
66
|
+
return await _codex_responses_handler(request, adapter)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.post("/responses", response_model=None, include_in_schema=False)
|
|
70
|
+
@with_format_chain(
|
|
71
|
+
[FORMAT_OPENAI_RESPONSES], endpoint=UPSTREAM_ENDPOINT_OPENAI_RESPONSES
|
|
72
|
+
)
|
|
73
|
+
async def codex_responses_legacy(
|
|
74
|
+
request: Request,
|
|
75
|
+
auth: ConditionalAuthDep,
|
|
76
|
+
adapter: CodexAdapterDep,
|
|
77
|
+
) -> StreamingResponse | Response | DeferredStreaming:
|
|
78
|
+
return await _codex_responses_handler(request, adapter)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post("/v1/chat/completions", response_model=None)
|
|
82
|
+
@with_format_chain(
|
|
83
|
+
[FORMAT_OPENAI_CHAT, FORMAT_OPENAI_RESPONSES],
|
|
84
|
+
endpoint=UPSTREAM_ENDPOINT_OPENAI_CHAT_COMPLETIONS,
|
|
85
|
+
)
|
|
86
|
+
async def codex_chat_completions(
|
|
87
|
+
request: Request,
|
|
88
|
+
auth: ConditionalAuthDep,
|
|
89
|
+
adapter: CodexAdapterDep,
|
|
90
|
+
) -> StreamingResponse | Response | DeferredStreaming:
|
|
91
|
+
return await handle_codex_request(request, adapter)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.get("/v1/models", response_model=None)
|
|
95
|
+
async def list_models(
|
|
96
|
+
request: Request,
|
|
97
|
+
auth: ConditionalAuthDep,
|
|
98
|
+
config: CodexConfigDep,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
"""List available Codex models."""
|
|
101
|
+
models = [card.model_dump(mode="json") for card in config.models_endpoint]
|
|
102
|
+
return {"object": "list", "data": models}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.post("/v1/messages", response_model=None)
|
|
106
|
+
@with_format_chain(
|
|
107
|
+
[FORMAT_ANTHROPIC_MESSAGES, FORMAT_OPENAI_RESPONSES],
|
|
108
|
+
endpoint=UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
|
|
109
|
+
)
|
|
110
|
+
async def codex_v1_messages(
|
|
111
|
+
request: Request,
|
|
112
|
+
auth: ConditionalAuthDep,
|
|
113
|
+
adapter: CodexAdapterDep,
|
|
114
|
+
) -> StreamingResponse | Response | DeferredStreaming:
|
|
115
|
+
return await handle_codex_request(request, adapter)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@router.post("/{session_id}/v1/messages", response_model=None)
|
|
119
|
+
@with_format_chain(
|
|
120
|
+
[FORMAT_ANTHROPIC_MESSAGES, FORMAT_OPENAI_RESPONSES],
|
|
121
|
+
endpoint="/{session_id}/v1/messages",
|
|
122
|
+
)
|
|
123
|
+
async def codex_v1_messages_with_session(
|
|
124
|
+
session_id: str,
|
|
125
|
+
request: Request,
|
|
126
|
+
auth: ConditionalAuthDep,
|
|
127
|
+
adapter: CodexAdapterDep,
|
|
128
|
+
) -> StreamingResponse | Response | DeferredStreaming:
|
|
129
|
+
return await handle_codex_request(request, adapter)
|