ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from fastapi import Request
|
|
9
|
+
from starlette.responses import JSONResponse, Response, StreamingResponse
|
|
10
|
+
|
|
11
|
+
from ccproxy.auth.exceptions import OAuthTokenRefreshError
|
|
12
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
13
|
+
from ccproxy.core.plugins.interfaces import (
|
|
14
|
+
DetectionServiceProtocol,
|
|
15
|
+
ProfiledTokenManagerProtocol,
|
|
16
|
+
)
|
|
17
|
+
from ccproxy.services.adapters.chain_composer import compose_from_chain
|
|
18
|
+
from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
|
|
19
|
+
from ccproxy.services.handler_config import HandlerConfig
|
|
20
|
+
from ccproxy.streaming import DeferredStreaming, StreamingBufferService
|
|
21
|
+
from ccproxy.utils.headers import (
|
|
22
|
+
extract_request_headers,
|
|
23
|
+
extract_response_headers,
|
|
24
|
+
filter_request_headers,
|
|
25
|
+
filter_response_headers,
|
|
26
|
+
)
|
|
27
|
+
from ccproxy.utils.model_mapper import restore_model_aliases
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = get_plugin_logger()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CodexAdapter(BaseHTTPAdapter):
|
|
34
|
+
"""Simplified Codex adapter."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
detection_service: DetectionServiceProtocol,
|
|
39
|
+
config: Any = None,
|
|
40
|
+
**kwargs: Any,
|
|
41
|
+
) -> None:
|
|
42
|
+
super().__init__(config=config, **kwargs)
|
|
43
|
+
self.detection_service: DetectionServiceProtocol = detection_service
|
|
44
|
+
self.token_manager: ProfiledTokenManagerProtocol = cast(
|
|
45
|
+
ProfiledTokenManagerProtocol, self.auth_manager
|
|
46
|
+
)
|
|
47
|
+
self.base_url = self.config.base_url.rstrip("/")
|
|
48
|
+
|
|
49
|
+
async def handle_request(
|
|
50
|
+
self, request: Request
|
|
51
|
+
) -> Response | StreamingResponse | DeferredStreaming:
|
|
52
|
+
"""Handle request with Codex-specific streaming behavior.
|
|
53
|
+
|
|
54
|
+
Codex upstream only supports streaming. If the client requests a non-streaming
|
|
55
|
+
response, we internally stream and buffer it, then return a standard Response.
|
|
56
|
+
"""
|
|
57
|
+
# Context + request info
|
|
58
|
+
ctx = request.state.context
|
|
59
|
+
self._ensure_tool_accumulator(ctx)
|
|
60
|
+
endpoint = ctx.metadata.get("endpoint", "")
|
|
61
|
+
body = await request.body()
|
|
62
|
+
body = await self._map_request_model(ctx, body)
|
|
63
|
+
headers = extract_request_headers(request)
|
|
64
|
+
|
|
65
|
+
# Determine client streaming intent from body flag (fallback to False)
|
|
66
|
+
wants_stream = False
|
|
67
|
+
try:
|
|
68
|
+
data = json.loads(body.decode()) if body else {}
|
|
69
|
+
wants_stream = bool(data.get("stream", False))
|
|
70
|
+
except Exception: # Malformed/missing JSON -> assume non-streaming
|
|
71
|
+
wants_stream = False
|
|
72
|
+
logger.trace(
|
|
73
|
+
"codex_adapter_request_intent",
|
|
74
|
+
wants_stream=wants_stream,
|
|
75
|
+
endpoint=endpoint,
|
|
76
|
+
format_chain=getattr(ctx, "format_chain", []),
|
|
77
|
+
category="streaming",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Explicitly set service_type for downstream helpers
|
|
81
|
+
with contextlib.suppress(Exception):
|
|
82
|
+
ctx.metadata.setdefault("service_type", "codex")
|
|
83
|
+
|
|
84
|
+
# If client wants streaming, delegate to streaming handler directly
|
|
85
|
+
if wants_stream and self.streaming_handler:
|
|
86
|
+
logger.trace(
|
|
87
|
+
"codex_adapter_delegating_streaming",
|
|
88
|
+
endpoint=endpoint,
|
|
89
|
+
category="streaming",
|
|
90
|
+
)
|
|
91
|
+
return await self.handle_streaming(request, endpoint)
|
|
92
|
+
|
|
93
|
+
# Otherwise, buffer the upstream streaming response into a standard one
|
|
94
|
+
if getattr(self.config, "buffer_non_streaming", True):
|
|
95
|
+
# 1) Prepare provider request (adds auth, sets stream=true, etc.)
|
|
96
|
+
# Apply request format conversion if specified
|
|
97
|
+
if ctx.format_chain and len(ctx.format_chain) > 1:
|
|
98
|
+
try:
|
|
99
|
+
request_payload = self._decode_json_body(
|
|
100
|
+
body, context="codex_request"
|
|
101
|
+
)
|
|
102
|
+
request_payload = await self._apply_format_chain(
|
|
103
|
+
data=request_payload,
|
|
104
|
+
format_chain=ctx.format_chain,
|
|
105
|
+
stage="request",
|
|
106
|
+
)
|
|
107
|
+
body = self._encode_json_body(request_payload)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(
|
|
110
|
+
"codex_format_chain_request_failed",
|
|
111
|
+
error=str(e),
|
|
112
|
+
exc_info=e,
|
|
113
|
+
category="transform",
|
|
114
|
+
)
|
|
115
|
+
return JSONResponse(
|
|
116
|
+
status_code=400,
|
|
117
|
+
content={
|
|
118
|
+
"error": {
|
|
119
|
+
"type": "invalid_request_error",
|
|
120
|
+
"message": "Failed to convert request using format chain",
|
|
121
|
+
"details": str(e),
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
prepared_body, prepared_headers = await self.prepare_provider_request(
|
|
127
|
+
body, headers, endpoint
|
|
128
|
+
)
|
|
129
|
+
logger.trace(
|
|
130
|
+
"codex_adapter_prepared_provider_request",
|
|
131
|
+
header_keys=list(prepared_headers.keys()),
|
|
132
|
+
body_size=len(prepared_body or b""),
|
|
133
|
+
category="http",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# 2) Build handler config using composed adapter from format_chain (unified path)
|
|
137
|
+
|
|
138
|
+
composed_adapter = (
|
|
139
|
+
compose_from_chain(
|
|
140
|
+
registry=self.format_registry, chain=ctx.format_chain
|
|
141
|
+
)
|
|
142
|
+
if self.format_registry and ctx.format_chain
|
|
143
|
+
else None
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
handler_config = HandlerConfig(
|
|
147
|
+
supports_streaming=True,
|
|
148
|
+
request_transformer=None,
|
|
149
|
+
response_adapter=composed_adapter,
|
|
150
|
+
format_context=None,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# 3) Use StreamingBufferService to convert upstream stream -> regular response
|
|
154
|
+
target_url = await self.get_target_url(endpoint)
|
|
155
|
+
# Try to use a client with base_url for better hook integration
|
|
156
|
+
http_client = await self.http_pool_manager.get_client()
|
|
157
|
+
hook_manager = (
|
|
158
|
+
getattr(self.streaming_handler, "hook_manager", None)
|
|
159
|
+
if self.streaming_handler
|
|
160
|
+
else None
|
|
161
|
+
)
|
|
162
|
+
buffer_service = StreamingBufferService(
|
|
163
|
+
http_client=http_client,
|
|
164
|
+
request_tracer=None,
|
|
165
|
+
hook_manager=hook_manager,
|
|
166
|
+
http_pool_manager=self.http_pool_manager,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
buffered_response = await buffer_service.handle_buffered_streaming_request(
|
|
170
|
+
method=request.method,
|
|
171
|
+
url=target_url,
|
|
172
|
+
headers=prepared_headers,
|
|
173
|
+
body=prepared_body,
|
|
174
|
+
handler_config=handler_config,
|
|
175
|
+
request_context=ctx,
|
|
176
|
+
provider_name="codex",
|
|
177
|
+
)
|
|
178
|
+
logger.trace(
|
|
179
|
+
"codex_adapter_buffered_response_ready",
|
|
180
|
+
status_code=buffered_response.status_code,
|
|
181
|
+
buffer_respones_preview=buffered_response.body[:300],
|
|
182
|
+
category="streaming",
|
|
183
|
+
format_chain=getattr(ctx, "format_chain", []),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# 4) Apply reverse format chain on buffered body if needed
|
|
187
|
+
if ctx.format_chain and len(ctx.format_chain) > 1:
|
|
188
|
+
from typing import Literal
|
|
189
|
+
|
|
190
|
+
mode: Literal["error", "response"] = (
|
|
191
|
+
"error" if buffered_response.status_code >= 400 else "response"
|
|
192
|
+
)
|
|
193
|
+
try:
|
|
194
|
+
body_bytes = (
|
|
195
|
+
buffered_response.body
|
|
196
|
+
if isinstance(buffered_response.body, bytes)
|
|
197
|
+
else bytes(buffered_response.body)
|
|
198
|
+
)
|
|
199
|
+
response_payload = self._decode_json_body(
|
|
200
|
+
body_bytes, context=f"codex_{mode}"
|
|
201
|
+
)
|
|
202
|
+
response_payload = await self._apply_format_chain(
|
|
203
|
+
data=response_payload,
|
|
204
|
+
format_chain=ctx.format_chain,
|
|
205
|
+
stage=mode,
|
|
206
|
+
)
|
|
207
|
+
metadata = getattr(ctx, "metadata", None)
|
|
208
|
+
alias_map = getattr(ctx, "_model_alias_map", None)
|
|
209
|
+
if isinstance(metadata, dict):
|
|
210
|
+
if (
|
|
211
|
+
isinstance(alias_map, dict)
|
|
212
|
+
and isinstance(response_payload, dict)
|
|
213
|
+
and isinstance(response_payload.get("model"), str)
|
|
214
|
+
):
|
|
215
|
+
response_payload["model"] = alias_map.get(
|
|
216
|
+
response_payload["model"], response_payload["model"]
|
|
217
|
+
)
|
|
218
|
+
restore_model_aliases(response_payload, metadata)
|
|
219
|
+
converted_body = self._encode_json_body(response_payload)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(
|
|
222
|
+
"codex_format_chain_response_failed",
|
|
223
|
+
error=str(e),
|
|
224
|
+
mode=mode,
|
|
225
|
+
exc_info=e,
|
|
226
|
+
category="transform",
|
|
227
|
+
)
|
|
228
|
+
return JSONResponse(
|
|
229
|
+
status_code=502,
|
|
230
|
+
content={
|
|
231
|
+
"error": {
|
|
232
|
+
"type": "server_error",
|
|
233
|
+
"message": "Failed to convert provider response using format chain",
|
|
234
|
+
"details": str(e),
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
headers_out = filter_response_headers(dict(buffered_response.headers))
|
|
240
|
+
return Response(
|
|
241
|
+
content=converted_body,
|
|
242
|
+
status_code=buffered_response.status_code,
|
|
243
|
+
headers=headers_out,
|
|
244
|
+
media_type="application/json",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# No conversion needed; return buffered response as-is
|
|
248
|
+
return buffered_response
|
|
249
|
+
|
|
250
|
+
# Fallback: no buffering requested, use base non-streaming flow
|
|
251
|
+
return await super().handle_request(request)
|
|
252
|
+
|
|
253
|
+
async def get_target_url(self, endpoint: str) -> str:
|
|
254
|
+
return f"{self.base_url}/responses"
|
|
255
|
+
|
|
256
|
+
async def prepare_provider_request(
|
|
257
|
+
self, body: bytes, headers: dict[str, str], endpoint: str
|
|
258
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
259
|
+
token_value = await self._resolve_access_token()
|
|
260
|
+
|
|
261
|
+
# Get profile to extract chatgpt_account_id
|
|
262
|
+
profile = await self.token_manager.get_profile_quick()
|
|
263
|
+
chatgpt_account_id = (
|
|
264
|
+
getattr(profile, "chatgpt_account_id", None) if profile else None
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Parse body (format conversion is now handled by format chain)
|
|
268
|
+
body_data = json.loads(body.decode()) if body else {}
|
|
269
|
+
|
|
270
|
+
# Inject instructions mandatory for being allow to
|
|
271
|
+
# to used the Codex API endpoint
|
|
272
|
+
# Fetch detected instructions from detection service
|
|
273
|
+
instructions = self._get_instructions()
|
|
274
|
+
|
|
275
|
+
existing_instructions = body_data.get("instructions")
|
|
276
|
+
if isinstance(existing_instructions, str) and existing_instructions:
|
|
277
|
+
if instructions:
|
|
278
|
+
instructions = instructions + "\n" + existing_instructions
|
|
279
|
+
else:
|
|
280
|
+
instructions = existing_instructions
|
|
281
|
+
|
|
282
|
+
body_data["instructions"] = instructions
|
|
283
|
+
|
|
284
|
+
# Codex backend requires stream=true, always override
|
|
285
|
+
body_data["stream"] = True
|
|
286
|
+
body_data["store"] = False
|
|
287
|
+
|
|
288
|
+
# Remove unsupported keys for Codex
|
|
289
|
+
for key in ("max_output_tokens", "max_completion_tokens", "temperature"):
|
|
290
|
+
body_data.pop(key, None)
|
|
291
|
+
|
|
292
|
+
list_input = body_data.get("input", [])
|
|
293
|
+
# Remove any input types that Codex does not support
|
|
294
|
+
body_data["input"] = [
|
|
295
|
+
input for input in list_input if input.get("type") != "item_reference"
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
#
|
|
299
|
+
# Remove any prefixed metadata fields that shouldn't be sent to the API
|
|
300
|
+
body_data = self._remove_metadata_fields(body_data)
|
|
301
|
+
|
|
302
|
+
# Filter and add headers
|
|
303
|
+
filtered_headers = filter_request_headers(headers, preserve_auth=False)
|
|
304
|
+
|
|
305
|
+
session_id = filtered_headers.get("session_id") or str(uuid.uuid4())
|
|
306
|
+
conversation_id = filtered_headers.get("conversation_id") or str(uuid.uuid4())
|
|
307
|
+
|
|
308
|
+
base_headers = {
|
|
309
|
+
"authorization": f"Bearer {token_value}",
|
|
310
|
+
"content-type": "application/json",
|
|
311
|
+
"session_id": session_id,
|
|
312
|
+
"conversation_id": conversation_id,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if chatgpt_account_id is not None:
|
|
316
|
+
base_headers["chatgpt-account-id"] = chatgpt_account_id
|
|
317
|
+
|
|
318
|
+
filtered_headers.update(base_headers)
|
|
319
|
+
|
|
320
|
+
cli_headers = self._collect_cli_headers()
|
|
321
|
+
if cli_headers:
|
|
322
|
+
filtered_headers.update(cli_headers)
|
|
323
|
+
|
|
324
|
+
return json.dumps(body_data).encode(), filtered_headers
|
|
325
|
+
|
|
326
|
+
async def process_provider_response(
|
|
327
|
+
self, response: httpx.Response, endpoint: str
|
|
328
|
+
) -> Response | StreamingResponse:
|
|
329
|
+
"""Return a plain Response; streaming handled upstream by BaseHTTPAdapter.
|
|
330
|
+
|
|
331
|
+
The BaseHTTPAdapter is responsible for detecting streaming and delegating
|
|
332
|
+
to the shared StreamingHandler. For non-streaming responses, adapters
|
|
333
|
+
should return a simple Starlette Response.
|
|
334
|
+
"""
|
|
335
|
+
response_headers = extract_response_headers(response)
|
|
336
|
+
return Response(
|
|
337
|
+
content=response.content,
|
|
338
|
+
status_code=response.status_code,
|
|
339
|
+
headers=response_headers,
|
|
340
|
+
media_type=response.headers.get("content-type"),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
async def _resolve_access_token(self) -> str:
|
|
344
|
+
"""Resolve an access token suitable for Codex requests.
|
|
345
|
+
|
|
346
|
+
If the auth manager/credential balancer is not configured, raise a
|
|
347
|
+
unified AuthenticationError so middleware can return a clean 401
|
|
348
|
+
without leaking stack traces.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
# Guard: token manager must be configured via plugin auth_manager
|
|
352
|
+
if not getattr(self, "token_manager", None):
|
|
353
|
+
from ccproxy.core.errors import AuthenticationError
|
|
354
|
+
|
|
355
|
+
logger.warning(
|
|
356
|
+
"auth_manager_override_not_resolved",
|
|
357
|
+
plugin="codex",
|
|
358
|
+
auth_manager_name="codex_credential_balancer",
|
|
359
|
+
category="auth",
|
|
360
|
+
)
|
|
361
|
+
raise AuthenticationError(
|
|
362
|
+
"Authentication manager not configured for Codex provider"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
token_manager = self.token_manager
|
|
366
|
+
|
|
367
|
+
async def _snapshot_token() -> str | None:
|
|
368
|
+
snapshot = await token_manager.get_token_snapshot()
|
|
369
|
+
if snapshot and snapshot.access_token:
|
|
370
|
+
return snapshot.access_token
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
credentials = await token_manager.load_credentials()
|
|
374
|
+
if credentials and token_manager.should_refresh(credentials):
|
|
375
|
+
try:
|
|
376
|
+
refreshed = await token_manager.get_access_token_with_refresh()
|
|
377
|
+
if refreshed:
|
|
378
|
+
return refreshed
|
|
379
|
+
except OAuthTokenRefreshError as exc:
|
|
380
|
+
logger.warning(
|
|
381
|
+
"codex_token_refresh_failed",
|
|
382
|
+
error=str(exc),
|
|
383
|
+
category="auth",
|
|
384
|
+
)
|
|
385
|
+
fallback = await _snapshot_token()
|
|
386
|
+
if fallback:
|
|
387
|
+
return fallback
|
|
388
|
+
|
|
389
|
+
token = None
|
|
390
|
+
try:
|
|
391
|
+
token = await token_manager.get_access_token()
|
|
392
|
+
except OAuthTokenRefreshError as exc:
|
|
393
|
+
logger.warning(
|
|
394
|
+
"codex_token_refresh_failed",
|
|
395
|
+
error=str(exc),
|
|
396
|
+
category="auth",
|
|
397
|
+
)
|
|
398
|
+
fallback = await _snapshot_token()
|
|
399
|
+
if fallback:
|
|
400
|
+
return fallback
|
|
401
|
+
|
|
402
|
+
if token:
|
|
403
|
+
return token
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
refreshed = await token_manager.get_access_token_with_refresh()
|
|
407
|
+
if refreshed:
|
|
408
|
+
return refreshed
|
|
409
|
+
except OAuthTokenRefreshError as exc:
|
|
410
|
+
logger.warning(
|
|
411
|
+
"codex_token_refresh_failed",
|
|
412
|
+
error=str(exc),
|
|
413
|
+
category="auth",
|
|
414
|
+
)
|
|
415
|
+
fallback = await _snapshot_token()
|
|
416
|
+
if fallback:
|
|
417
|
+
return fallback
|
|
418
|
+
|
|
419
|
+
fallback = await _snapshot_token()
|
|
420
|
+
if fallback:
|
|
421
|
+
return fallback
|
|
422
|
+
|
|
423
|
+
raise ValueError("No authentication credentials available")
|
|
424
|
+
|
|
425
|
+
def _collect_cli_headers(self) -> dict[str, str]:
|
|
426
|
+
"""Collect safe CLI headers from detection cache for forwarding."""
|
|
427
|
+
|
|
428
|
+
if not self.detection_service:
|
|
429
|
+
return {}
|
|
430
|
+
|
|
431
|
+
headers_data = self.detection_service.get_detected_headers()
|
|
432
|
+
if not headers_data:
|
|
433
|
+
return {}
|
|
434
|
+
|
|
435
|
+
ignores = {
|
|
436
|
+
header.lower() for header in self.detection_service.get_ignored_headers()
|
|
437
|
+
}
|
|
438
|
+
redacted = {
|
|
439
|
+
header.lower() for header in self.detection_service.get_redacted_headers()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return headers_data.filtered(ignores=ignores, redacted=redacted)
|
|
443
|
+
|
|
444
|
+
async def handle_streaming(
|
|
445
|
+
self, request: Request, endpoint: str, **kwargs: Any
|
|
446
|
+
) -> StreamingResponse | DeferredStreaming:
|
|
447
|
+
"""Handle streaming with request conversion for Codex.
|
|
448
|
+
|
|
449
|
+
Applies request format conversion (e.g., anthropic.messages -> openai.responses) before
|
|
450
|
+
preparing the provider request, then delegates to StreamingHandler with
|
|
451
|
+
a streaming response adapter for reverse conversion as needed.
|
|
452
|
+
"""
|
|
453
|
+
if not self.streaming_handler:
|
|
454
|
+
# Fallback to base behavior
|
|
455
|
+
return await super().handle_streaming(request, endpoint, **kwargs)
|
|
456
|
+
|
|
457
|
+
# Get context
|
|
458
|
+
ctx = request.state.context
|
|
459
|
+
self._ensure_tool_accumulator(ctx)
|
|
460
|
+
|
|
461
|
+
# Extract body and headers
|
|
462
|
+
body = await request.body()
|
|
463
|
+
body = await self._map_request_model(ctx, body)
|
|
464
|
+
headers = extract_request_headers(request)
|
|
465
|
+
|
|
466
|
+
# Ensure format adapters are available when required
|
|
467
|
+
self._ensure_format_registry(ctx.format_chain, endpoint)
|
|
468
|
+
|
|
469
|
+
# Apply request format conversion if a chain is defined
|
|
470
|
+
if ctx.format_chain and len(ctx.format_chain) > 1:
|
|
471
|
+
try:
|
|
472
|
+
request_payload = self._decode_json_body(
|
|
473
|
+
body, context="codex_stream_request"
|
|
474
|
+
)
|
|
475
|
+
request_payload = await self._apply_format_chain(
|
|
476
|
+
data=request_payload,
|
|
477
|
+
format_chain=ctx.format_chain,
|
|
478
|
+
stage="request",
|
|
479
|
+
)
|
|
480
|
+
self._record_tool_definitions(ctx, request_payload)
|
|
481
|
+
body = self._encode_json_body(request_payload)
|
|
482
|
+
except Exception as e:
|
|
483
|
+
logger.error(
|
|
484
|
+
"codex_format_chain_request_failed",
|
|
485
|
+
error=str(e),
|
|
486
|
+
exc_info=e,
|
|
487
|
+
category="transform",
|
|
488
|
+
)
|
|
489
|
+
# Convert error to streaming response
|
|
490
|
+
|
|
491
|
+
error_content = {
|
|
492
|
+
"error": {
|
|
493
|
+
"type": "invalid_request_error",
|
|
494
|
+
"message": "Failed to convert request using format chain",
|
|
495
|
+
"details": str(e),
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
error_bytes = json.dumps(error_content).encode("utf-8")
|
|
499
|
+
|
|
500
|
+
async def error_generator() -> (
|
|
501
|
+
Any
|
|
502
|
+
): # AsyncGenerator[bytes, None] would be more specific
|
|
503
|
+
yield error_bytes
|
|
504
|
+
|
|
505
|
+
return StreamingResponse(
|
|
506
|
+
content=error_generator(),
|
|
507
|
+
status_code=400,
|
|
508
|
+
media_type="application/json",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Provider-specific preparation (adds auth, sets stream=true)
|
|
512
|
+
prepared_body, prepared_headers = await self.prepare_provider_request(
|
|
513
|
+
body, headers, endpoint
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Get format adapter for streaming reverse conversion
|
|
517
|
+
streaming_format_adapter = None
|
|
518
|
+
if ctx.format_chain and len(ctx.format_chain) > 1 and self.format_registry:
|
|
519
|
+
from_format = ctx.format_chain[-1]
|
|
520
|
+
to_format = ctx.format_chain[0]
|
|
521
|
+
try:
|
|
522
|
+
streaming_format_adapter = self.format_registry.get_if_exists(
|
|
523
|
+
from_format, to_format
|
|
524
|
+
)
|
|
525
|
+
except Exception:
|
|
526
|
+
streaming_format_adapter = None
|
|
527
|
+
|
|
528
|
+
handler_config = HandlerConfig(
|
|
529
|
+
supports_streaming=True,
|
|
530
|
+
request_transformer=None,
|
|
531
|
+
response_adapter=streaming_format_adapter,
|
|
532
|
+
format_context=None,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
target_url = await self.get_target_url(endpoint)
|
|
536
|
+
|
|
537
|
+
parsed_url = urlparse(target_url)
|
|
538
|
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
539
|
+
|
|
540
|
+
return await self.streaming_handler.handle_streaming_request(
|
|
541
|
+
method=request.method,
|
|
542
|
+
url=target_url,
|
|
543
|
+
headers=prepared_headers,
|
|
544
|
+
body=prepared_body,
|
|
545
|
+
handler_config=handler_config,
|
|
546
|
+
request_context=ctx,
|
|
547
|
+
client=await self.http_pool_manager.get_client(base_url=base_url),
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Helper methods
|
|
551
|
+
def _remove_metadata_fields(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
552
|
+
"""Remove fields that start with '_' as they are internal metadata.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
data: Dictionary that may contain metadata fields
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Cleaned dictionary without metadata fields
|
|
559
|
+
"""
|
|
560
|
+
if not isinstance(data, dict):
|
|
561
|
+
return data
|
|
562
|
+
|
|
563
|
+
# Create a new dict without keys starting with '_'
|
|
564
|
+
cleaned_data: dict[str, Any] = {}
|
|
565
|
+
for key, value in data.items():
|
|
566
|
+
if not key.startswith("_"):
|
|
567
|
+
# Recursively clean nested dictionaries
|
|
568
|
+
if isinstance(value, dict):
|
|
569
|
+
cleaned_data[key] = self._remove_metadata_fields(value)
|
|
570
|
+
elif isinstance(value, list):
|
|
571
|
+
# Clean list items if they are dictionaries
|
|
572
|
+
cleaned_items: list[Any] = []
|
|
573
|
+
for item in value:
|
|
574
|
+
if isinstance(item, dict):
|
|
575
|
+
cleaned_items.append(self._remove_metadata_fields(item))
|
|
576
|
+
else:
|
|
577
|
+
cleaned_items.append(item)
|
|
578
|
+
cleaned_data[key] = cleaned_items
|
|
579
|
+
else:
|
|
580
|
+
cleaned_data[key] = value
|
|
581
|
+
|
|
582
|
+
return cleaned_data
|
|
583
|
+
|
|
584
|
+
def _get_instructions(self) -> str:
|
|
585
|
+
if not self.detection_service:
|
|
586
|
+
return ""
|
|
587
|
+
|
|
588
|
+
prompts = self.detection_service.get_detected_prompts()
|
|
589
|
+
if prompts.has_instructions():
|
|
590
|
+
return prompts.instructions or ""
|
|
591
|
+
|
|
592
|
+
injection = self.detection_service.get_system_prompt()
|
|
593
|
+
if isinstance(injection, dict):
|
|
594
|
+
instructions = injection.get("instructions")
|
|
595
|
+
if isinstance(instructions, str):
|
|
596
|
+
return instructions
|
|
597
|
+
|
|
598
|
+
fallback = getattr(self.detection_service, "instructions_value", None)
|
|
599
|
+
if isinstance(fallback, str):
|
|
600
|
+
return fallback
|
|
601
|
+
|
|
602
|
+
return ""
|
|
603
|
+
|
|
604
|
+
def adapt_error(self, error_body: dict[str, Any]) -> dict[str, Any]:
|
|
605
|
+
"""Convert Codex error format to appropriate API error format.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
error_body: Codex error response
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
API-formatted error response
|
|
612
|
+
"""
|
|
613
|
+
# Handle the specific "Stream must be set to true" error
|
|
614
|
+
if isinstance(error_body, dict) and "detail" in error_body:
|
|
615
|
+
detail = error_body["detail"]
|
|
616
|
+
if "Stream must be set to true" in detail:
|
|
617
|
+
# Convert to generic invalid request error
|
|
618
|
+
return {
|
|
619
|
+
"error": {
|
|
620
|
+
"type": "invalid_request_error",
|
|
621
|
+
"message": "Invalid streaming parameter",
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
# Handle other error formats that might have "error" key
|
|
626
|
+
if "error" in error_body:
|
|
627
|
+
return error_body
|
|
628
|
+
|
|
629
|
+
# Default: wrap non-standard errors
|
|
630
|
+
return {
|
|
631
|
+
"error": {
|
|
632
|
+
"type": "internal_server_error",
|
|
633
|
+
"message": "An error occurred processing the request",
|
|
634
|
+
}
|
|
635
|
+
}
|