ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"""Claude SDK adapter implementation using delegation pattern."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from fastapi import HTTPException, Request
|
|
11
|
+
from starlette.requests import Request as StarletteRequest
|
|
12
|
+
from starlette.responses import Response, StreamingResponse
|
|
13
|
+
|
|
14
|
+
from ccproxy.config.utils import OPENAI_CHAT_COMPLETIONS_PATH
|
|
15
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
16
|
+
from ccproxy.core.request_context import RequestContext
|
|
17
|
+
from ccproxy.llms.streaming import OpenAIStreamProcessor
|
|
18
|
+
from ccproxy.services.adapters.chain_composer import compose_from_chain
|
|
19
|
+
from ccproxy.services.adapters.format_adapter import FormatAdapterProtocol
|
|
20
|
+
from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
|
|
21
|
+
from ccproxy.streaming import DeferredStreaming
|
|
22
|
+
from ccproxy.streaming.sse import serialize_json_to_sse_stream
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from ccproxy.services.interfaces import IMetricsCollector
|
|
27
|
+
|
|
28
|
+
from .auth import NoOpAuthManager
|
|
29
|
+
from .config import ClaudeSDKSettings
|
|
30
|
+
from .handler import ClaudeSDKHandler
|
|
31
|
+
from .manager import SessionManager
|
|
32
|
+
from .models import MessageResponse
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = get_plugin_logger()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ClaudeSDKAdapter(BaseHTTPAdapter):
|
|
39
|
+
"""Claude SDK adapter implementation using delegation pattern.
|
|
40
|
+
|
|
41
|
+
This adapter integrates with the application request lifecycle,
|
|
42
|
+
following the same pattern as claude_api and codex plugins.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
config: ClaudeSDKSettings,
|
|
48
|
+
# Optional dependencies
|
|
49
|
+
session_manager: SessionManager | None = None,
|
|
50
|
+
metrics: "IMetricsCollector | None" = None,
|
|
51
|
+
hook_manager: Any | None = None,
|
|
52
|
+
**kwargs: Any,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Initialize the Claude SDK adapter with explicit dependencies.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
config: SDK configuration settings
|
|
58
|
+
session_manager: Optional session manager for session handling
|
|
59
|
+
metrics: Optional metrics collector
|
|
60
|
+
hook_manager: Optional hook manager for emitting events
|
|
61
|
+
"""
|
|
62
|
+
# Initialize BaseHTTPAdapter with dummy auth_manager and http_pool_manager
|
|
63
|
+
# since ClaudeSDK doesn't use external HTTP
|
|
64
|
+
super().__init__(
|
|
65
|
+
config=config, auth_manager=None, http_pool_manager=None, **kwargs
|
|
66
|
+
)
|
|
67
|
+
self.metrics = metrics
|
|
68
|
+
self.hook_manager = hook_manager
|
|
69
|
+
|
|
70
|
+
# Generate or set default session ID
|
|
71
|
+
self._runtime_default_session_id = None
|
|
72
|
+
if (
|
|
73
|
+
config.auto_generate_default_session
|
|
74
|
+
and config.sdk_session_pool
|
|
75
|
+
and config.sdk_session_pool.enabled
|
|
76
|
+
):
|
|
77
|
+
# Generate a random session ID for this runtime
|
|
78
|
+
self._runtime_default_session_id = f"auto-{uuid.uuid4().hex[:12]}"
|
|
79
|
+
logger.debug(
|
|
80
|
+
"auto_generated_session",
|
|
81
|
+
session_id=self._runtime_default_session_id,
|
|
82
|
+
lifetime="runtime",
|
|
83
|
+
)
|
|
84
|
+
elif config.default_session_id:
|
|
85
|
+
self._runtime_default_session_id = config.default_session_id
|
|
86
|
+
logger.debug(
|
|
87
|
+
"using_configured_default_session",
|
|
88
|
+
session_id=self._runtime_default_session_id,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Use provided session_manager or create if needed and enabled
|
|
92
|
+
if (
|
|
93
|
+
session_manager is None
|
|
94
|
+
and config.sdk_session_pool
|
|
95
|
+
and config.sdk_session_pool.enabled
|
|
96
|
+
):
|
|
97
|
+
session_manager = SessionManager(config=config)
|
|
98
|
+
logger.debug(
|
|
99
|
+
"adapter_session_pool_enabled",
|
|
100
|
+
session_ttl=config.sdk_session_pool.session_ttl,
|
|
101
|
+
max_sessions=config.sdk_session_pool.max_sessions,
|
|
102
|
+
has_default_session=bool(self._runtime_default_session_id),
|
|
103
|
+
auto_generated=config.auto_generate_default_session,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.session_manager = session_manager
|
|
107
|
+
self.handler: ClaudeSDKHandler | None = ClaudeSDKHandler(
|
|
108
|
+
config=config,
|
|
109
|
+
session_manager=session_manager,
|
|
110
|
+
hook_manager=hook_manager,
|
|
111
|
+
)
|
|
112
|
+
self.auth_manager = NoOpAuthManager()
|
|
113
|
+
self._detection_service: Any | None = None
|
|
114
|
+
self._initialized = False
|
|
115
|
+
self._format_adapter_cache: dict[tuple[str, ...], FormatAdapterProtocol] = {}
|
|
116
|
+
|
|
117
|
+
async def initialize(self) -> None:
|
|
118
|
+
"""Initialize the adapter and start session manager if needed."""
|
|
119
|
+
if not self._initialized:
|
|
120
|
+
if self.session_manager:
|
|
121
|
+
await self.session_manager.start()
|
|
122
|
+
logger.debug("session_manager_started")
|
|
123
|
+
self._initialized = True
|
|
124
|
+
|
|
125
|
+
def set_detection_service(self, detection_service: Any) -> None:
|
|
126
|
+
"""Set the detection service.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
detection_service: Claude CLI detection service
|
|
130
|
+
"""
|
|
131
|
+
self._detection_service = detection_service
|
|
132
|
+
|
|
133
|
+
def _resolve_format_adapter(
|
|
134
|
+
self, format_chain: list[str]
|
|
135
|
+
) -> FormatAdapterProtocol | None:
|
|
136
|
+
"""Return a composed format adapter for the provided chain."""
|
|
137
|
+
|
|
138
|
+
if not self.format_registry or len(format_chain) < 2:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
key = tuple(format_chain)
|
|
142
|
+
adapter = self._format_adapter_cache.get(key)
|
|
143
|
+
if adapter is not None:
|
|
144
|
+
return adapter
|
|
145
|
+
|
|
146
|
+
adapter = compose_from_chain(
|
|
147
|
+
registry=self.format_registry,
|
|
148
|
+
chain=format_chain,
|
|
149
|
+
name=f"claude_sdk_adapter_{'__'.join(format_chain)}",
|
|
150
|
+
)
|
|
151
|
+
self._format_adapter_cache[key] = adapter
|
|
152
|
+
return adapter
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
async def _single_payload_stream(
|
|
156
|
+
payload: dict[str, Any],
|
|
157
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
158
|
+
yield payload
|
|
159
|
+
|
|
160
|
+
async def handle_request(
|
|
161
|
+
self, request: Request
|
|
162
|
+
) -> Response | StreamingResponse | DeferredStreaming:
|
|
163
|
+
# Ensure adapter is initialized
|
|
164
|
+
await self.initialize()
|
|
165
|
+
|
|
166
|
+
# Extract endpoint from request URL
|
|
167
|
+
endpoint = request.url.path
|
|
168
|
+
method = request.method
|
|
169
|
+
|
|
170
|
+
# Parse request body
|
|
171
|
+
body = await request.body()
|
|
172
|
+
if not body:
|
|
173
|
+
raise HTTPException(status_code=400, detail="Request body is required")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
request_data = json.loads(body)
|
|
177
|
+
except json.JSONDecodeError as e:
|
|
178
|
+
raise HTTPException(
|
|
179
|
+
status_code=400, detail=f"Invalid JSON: {str(e)}"
|
|
180
|
+
) from e
|
|
181
|
+
|
|
182
|
+
request_context: RequestContext | None = RequestContext.get_current()
|
|
183
|
+
if not request_context:
|
|
184
|
+
raise HTTPException(
|
|
185
|
+
status_code=500,
|
|
186
|
+
detail=(
|
|
187
|
+
"RequestContext not available - plugin must be invoked within the "
|
|
188
|
+
"application request lifecycle"
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self._ensure_tool_accumulator(request_context)
|
|
193
|
+
|
|
194
|
+
format_chain = list(getattr(request_context, "format_chain", []) or [])
|
|
195
|
+
try:
|
|
196
|
+
format_adapter = self._resolve_format_adapter(format_chain)
|
|
197
|
+
except Exception as exc: # pragma: no cover - defensive logging in production
|
|
198
|
+
logger.error(
|
|
199
|
+
"format_adapter_resolution_failed",
|
|
200
|
+
error=str(exc),
|
|
201
|
+
format_chain=format_chain,
|
|
202
|
+
endpoint=endpoint,
|
|
203
|
+
category="format",
|
|
204
|
+
exc_info=exc,
|
|
205
|
+
)
|
|
206
|
+
raise HTTPException(
|
|
207
|
+
status_code=500,
|
|
208
|
+
detail="Failed to prepare format adapter for Claude SDK request",
|
|
209
|
+
) from exc
|
|
210
|
+
|
|
211
|
+
if format_adapter:
|
|
212
|
+
try:
|
|
213
|
+
request_data = await format_adapter.convert_request(request_data)
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
logger.error(
|
|
216
|
+
"format_request_conversion_failed",
|
|
217
|
+
error=str(exc),
|
|
218
|
+
format_chain=format_chain,
|
|
219
|
+
endpoint=endpoint,
|
|
220
|
+
category="format",
|
|
221
|
+
exc_info=exc,
|
|
222
|
+
)
|
|
223
|
+
raise HTTPException(
|
|
224
|
+
status_code=400,
|
|
225
|
+
detail="Failed to convert request payload for Claude SDK",
|
|
226
|
+
) from exc
|
|
227
|
+
|
|
228
|
+
# Check if format conversion is needed (OpenAI to Anthropic)
|
|
229
|
+
# The endpoint will contain the path after the prefix, e.g., "/v1/chat/completions"
|
|
230
|
+
needs_conversion = bool(format_adapter) or endpoint.endswith(
|
|
231
|
+
OPENAI_CHAT_COMPLETIONS_PATH
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Extract parameters for SDK handler
|
|
235
|
+
messages = request_data.get("messages", [])
|
|
236
|
+
model = request_data.get("model", "claude-3-opus-20240229")
|
|
237
|
+
temperature = request_data.get("temperature")
|
|
238
|
+
max_tokens = request_data.get("max_tokens")
|
|
239
|
+
stream = request_data.get("stream", False)
|
|
240
|
+
|
|
241
|
+
# Get session_id from multiple sources (in priority order):
|
|
242
|
+
# 1. URL path (stored in request.state by the route handler)
|
|
243
|
+
# 2. Query parameters
|
|
244
|
+
# 3. Request body
|
|
245
|
+
# 4. Default from config (if session pool is enabled)
|
|
246
|
+
session_id = getattr(request.state, "session_id", None)
|
|
247
|
+
source = "path" if session_id else None
|
|
248
|
+
|
|
249
|
+
if not session_id and request.query_params:
|
|
250
|
+
session_id = request.query_params.get("session_id")
|
|
251
|
+
source = "query" if session_id else None
|
|
252
|
+
|
|
253
|
+
if not session_id:
|
|
254
|
+
session_id = request_data.get("session_id")
|
|
255
|
+
source = "body" if session_id else None
|
|
256
|
+
|
|
257
|
+
if (
|
|
258
|
+
not session_id
|
|
259
|
+
and self._runtime_default_session_id
|
|
260
|
+
and self.config.sdk_session_pool
|
|
261
|
+
and self.config.sdk_session_pool.enabled
|
|
262
|
+
):
|
|
263
|
+
# Use runtime default session_id (either configured or auto-generated)
|
|
264
|
+
session_id = self._runtime_default_session_id
|
|
265
|
+
source = (
|
|
266
|
+
"default"
|
|
267
|
+
if not self.config.auto_generate_default_session
|
|
268
|
+
else "auto-generated"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Log session_id source for debugging
|
|
272
|
+
if session_id:
|
|
273
|
+
logger.debug(
|
|
274
|
+
"session_id_extracted",
|
|
275
|
+
session_id=session_id,
|
|
276
|
+
source=source,
|
|
277
|
+
has_default_configured=bool(self.config.default_session_id),
|
|
278
|
+
auto_generate_enabled=self.config.auto_generate_default_session,
|
|
279
|
+
runtime_default=self._runtime_default_session_id,
|
|
280
|
+
session_pool_enabled=bool(
|
|
281
|
+
self.config.sdk_session_pool
|
|
282
|
+
and self.config.sdk_session_pool.enabled
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Update context with claude_sdk specific metadata
|
|
287
|
+
request_context.metadata.update(
|
|
288
|
+
{
|
|
289
|
+
"provider": "claude_sdk",
|
|
290
|
+
"service_type": "claude_sdk",
|
|
291
|
+
"endpoint": endpoint.rstrip("/").split("/")[-1]
|
|
292
|
+
if endpoint
|
|
293
|
+
else "messages",
|
|
294
|
+
"model": model,
|
|
295
|
+
"stream": stream,
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
logger.info(
|
|
300
|
+
"plugin_request",
|
|
301
|
+
plugin="claude_sdk",
|
|
302
|
+
endpoint=endpoint,
|
|
303
|
+
model=model,
|
|
304
|
+
is_streaming=stream,
|
|
305
|
+
needs_conversion=needs_conversion,
|
|
306
|
+
session_id=session_id,
|
|
307
|
+
target_url=f"claude-sdk://{session_id}"
|
|
308
|
+
if session_id
|
|
309
|
+
else "claude-sdk://direct",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
# Call handler directly to create completion
|
|
314
|
+
if not self.handler:
|
|
315
|
+
raise HTTPException(status_code=503, detail="Handler not initialized")
|
|
316
|
+
|
|
317
|
+
result = await self.handler.create_completion(
|
|
318
|
+
request_context=request_context,
|
|
319
|
+
messages=messages,
|
|
320
|
+
model=model,
|
|
321
|
+
temperature=temperature,
|
|
322
|
+
max_tokens=max_tokens,
|
|
323
|
+
stream=stream,
|
|
324
|
+
session_id=session_id,
|
|
325
|
+
**{
|
|
326
|
+
k: v
|
|
327
|
+
for k, v in request_data.items()
|
|
328
|
+
if k
|
|
329
|
+
not in [
|
|
330
|
+
"messages",
|
|
331
|
+
"model",
|
|
332
|
+
"temperature",
|
|
333
|
+
"max_tokens",
|
|
334
|
+
"stream",
|
|
335
|
+
"session_id",
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if stream:
|
|
341
|
+
# Return streaming response
|
|
342
|
+
stream_result = cast(AsyncIterator[dict[str, Any]], result)
|
|
343
|
+
|
|
344
|
+
if format_adapter:
|
|
345
|
+
logger.debug(
|
|
346
|
+
"format_stream_adapter_applied",
|
|
347
|
+
format_chain=format_chain,
|
|
348
|
+
endpoint=endpoint,
|
|
349
|
+
category="format",
|
|
350
|
+
)
|
|
351
|
+
try:
|
|
352
|
+
converted_stream = format_adapter.convert_stream(stream_result)
|
|
353
|
+
except Exception as exc:
|
|
354
|
+
logger.error(
|
|
355
|
+
"format_stream_conversion_failed",
|
|
356
|
+
error=str(exc),
|
|
357
|
+
format_chain=format_chain,
|
|
358
|
+
endpoint=endpoint,
|
|
359
|
+
category="format",
|
|
360
|
+
exc_info=exc,
|
|
361
|
+
)
|
|
362
|
+
raise HTTPException(
|
|
363
|
+
status_code=500,
|
|
364
|
+
detail="Failed to convert Claude SDK streaming payload",
|
|
365
|
+
) from exc
|
|
366
|
+
|
|
367
|
+
async def adapted_stream_generator() -> AsyncIterator[bytes]:
|
|
368
|
+
"""Generate SSE stream from converted OpenAI-style chunks."""
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
async for sse_chunk in serialize_json_to_sse_stream(
|
|
372
|
+
converted_stream,
|
|
373
|
+
include_done=bool(
|
|
374
|
+
format_chain
|
|
375
|
+
and format_chain[0].startswith("openai.")
|
|
376
|
+
),
|
|
377
|
+
request_context=request_context,
|
|
378
|
+
):
|
|
379
|
+
yield sse_chunk
|
|
380
|
+
except asyncio.CancelledError as exc:
|
|
381
|
+
logger.warning(
|
|
382
|
+
"streaming_cancelled",
|
|
383
|
+
error=str(exc),
|
|
384
|
+
exc_info=exc,
|
|
385
|
+
category="streaming",
|
|
386
|
+
)
|
|
387
|
+
raise
|
|
388
|
+
except httpx.TimeoutException as exc:
|
|
389
|
+
logger.error(
|
|
390
|
+
"streaming_timeout",
|
|
391
|
+
error=str(exc),
|
|
392
|
+
exc_info=exc,
|
|
393
|
+
category="streaming",
|
|
394
|
+
)
|
|
395
|
+
error_stream = serialize_json_to_sse_stream(
|
|
396
|
+
self._single_payload_stream(
|
|
397
|
+
{"error": "Request timed out"}
|
|
398
|
+
),
|
|
399
|
+
include_done=False,
|
|
400
|
+
request_context=request_context,
|
|
401
|
+
)
|
|
402
|
+
async for error_chunk in error_stream:
|
|
403
|
+
yield error_chunk
|
|
404
|
+
except httpx.HTTPError as exc:
|
|
405
|
+
logger.error(
|
|
406
|
+
"streaming_http_error",
|
|
407
|
+
error=str(exc),
|
|
408
|
+
status_code=getattr(exc.response, "status_code", None)
|
|
409
|
+
if hasattr(exc, "response")
|
|
410
|
+
else None,
|
|
411
|
+
exc_info=exc,
|
|
412
|
+
category="streaming",
|
|
413
|
+
)
|
|
414
|
+
error_stream = serialize_json_to_sse_stream(
|
|
415
|
+
self._single_payload_stream(
|
|
416
|
+
{"error": f"HTTP error: {exc}"}
|
|
417
|
+
),
|
|
418
|
+
include_done=False,
|
|
419
|
+
request_context=request_context,
|
|
420
|
+
)
|
|
421
|
+
async for error_chunk in error_stream:
|
|
422
|
+
yield error_chunk
|
|
423
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
424
|
+
logger.error(
|
|
425
|
+
"streaming_unexpected_error",
|
|
426
|
+
error=str(exc),
|
|
427
|
+
exc_info=exc,
|
|
428
|
+
category="streaming",
|
|
429
|
+
)
|
|
430
|
+
error_stream = serialize_json_to_sse_stream(
|
|
431
|
+
self._single_payload_stream({"error": str(exc)}),
|
|
432
|
+
include_done=False,
|
|
433
|
+
request_context=request_context,
|
|
434
|
+
)
|
|
435
|
+
async for error_chunk in error_stream:
|
|
436
|
+
yield error_chunk
|
|
437
|
+
|
|
438
|
+
return StreamingResponse(
|
|
439
|
+
content=adapted_stream_generator(),
|
|
440
|
+
media_type="text/event-stream",
|
|
441
|
+
headers={
|
|
442
|
+
"Cache-Control": "no-cache",
|
|
443
|
+
"Connection": "keep-alive",
|
|
444
|
+
"X-Claude-SDK-Response": "true",
|
|
445
|
+
},
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
logger.debug(
|
|
449
|
+
"format_stream_adapter_not_used",
|
|
450
|
+
reason="no_format_adapter" if not format_adapter else "fallback",
|
|
451
|
+
format_chain=format_chain,
|
|
452
|
+
endpoint=endpoint,
|
|
453
|
+
category="format",
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
async def stream_generator() -> AsyncIterator[bytes]:
|
|
457
|
+
"""Handle passthrough or OpenAI-format streaming."""
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
if needs_conversion:
|
|
461
|
+
processor = OpenAIStreamProcessor(
|
|
462
|
+
model=model,
|
|
463
|
+
enable_usage=True,
|
|
464
|
+
enable_tool_calls=True,
|
|
465
|
+
output_format="sse",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
async for sse_chunk in processor.process_stream(
|
|
469
|
+
stream_result
|
|
470
|
+
):
|
|
471
|
+
if isinstance(sse_chunk, bytes):
|
|
472
|
+
yield sse_chunk
|
|
473
|
+
else:
|
|
474
|
+
yield str(sse_chunk).encode()
|
|
475
|
+
else:
|
|
476
|
+
async for chunk in serialize_json_to_sse_stream(
|
|
477
|
+
stream_result,
|
|
478
|
+
include_done=True,
|
|
479
|
+
request_context=request_context,
|
|
480
|
+
):
|
|
481
|
+
if isinstance(chunk, bytes):
|
|
482
|
+
yield chunk
|
|
483
|
+
else:
|
|
484
|
+
yield str(chunk).encode()
|
|
485
|
+
except asyncio.CancelledError as exc:
|
|
486
|
+
logger.warning(
|
|
487
|
+
"streaming_cancelled",
|
|
488
|
+
error=str(exc),
|
|
489
|
+
exc_info=exc,
|
|
490
|
+
category="streaming",
|
|
491
|
+
)
|
|
492
|
+
raise
|
|
493
|
+
except httpx.TimeoutException as exc:
|
|
494
|
+
logger.error(
|
|
495
|
+
"streaming_timeout",
|
|
496
|
+
error=str(exc),
|
|
497
|
+
exc_info=exc,
|
|
498
|
+
category="streaming",
|
|
499
|
+
)
|
|
500
|
+
async for error_chunk in serialize_json_to_sse_stream(
|
|
501
|
+
self._single_payload_stream({"error": "Request timed out"}),
|
|
502
|
+
include_done=False,
|
|
503
|
+
request_context=request_context,
|
|
504
|
+
):
|
|
505
|
+
yield error_chunk
|
|
506
|
+
except httpx.HTTPError as exc:
|
|
507
|
+
logger.error(
|
|
508
|
+
"streaming_http_error",
|
|
509
|
+
error=str(exc),
|
|
510
|
+
status_code=getattr(exc.response, "status_code", None)
|
|
511
|
+
if hasattr(exc, "response")
|
|
512
|
+
else None,
|
|
513
|
+
exc_info=exc,
|
|
514
|
+
category="streaming",
|
|
515
|
+
)
|
|
516
|
+
async for error_chunk in serialize_json_to_sse_stream(
|
|
517
|
+
self._single_payload_stream(
|
|
518
|
+
{"error": f"HTTP error: {exc}"}
|
|
519
|
+
),
|
|
520
|
+
include_done=False,
|
|
521
|
+
request_context=request_context,
|
|
522
|
+
):
|
|
523
|
+
yield error_chunk
|
|
524
|
+
except Exception as exc:
|
|
525
|
+
logger.error(
|
|
526
|
+
"streaming_unexpected_error",
|
|
527
|
+
error=str(exc),
|
|
528
|
+
exc_info=exc,
|
|
529
|
+
category="streaming",
|
|
530
|
+
)
|
|
531
|
+
async for error_chunk in serialize_json_to_sse_stream(
|
|
532
|
+
self._single_payload_stream({"error": str(exc)}),
|
|
533
|
+
include_done=False,
|
|
534
|
+
request_context=request_context,
|
|
535
|
+
):
|
|
536
|
+
yield error_chunk
|
|
537
|
+
|
|
538
|
+
return StreamingResponse(
|
|
539
|
+
content=stream_generator(),
|
|
540
|
+
media_type="text/event-stream",
|
|
541
|
+
headers={
|
|
542
|
+
"Cache-Control": "no-cache",
|
|
543
|
+
"Connection": "keep-alive",
|
|
544
|
+
"X-Claude-SDK-Response": "true",
|
|
545
|
+
},
|
|
546
|
+
)
|
|
547
|
+
else:
|
|
548
|
+
# Convert MessageResponse to dict for JSON response
|
|
549
|
+
if isinstance(result, MessageResponse):
|
|
550
|
+
response_data = result.model_dump()
|
|
551
|
+
else:
|
|
552
|
+
# This shouldn't happen when stream=False, but handle it
|
|
553
|
+
response_data = cast(dict[str, Any], result)
|
|
554
|
+
|
|
555
|
+
# Convert to OpenAI format if needed
|
|
556
|
+
if format_adapter:
|
|
557
|
+
try:
|
|
558
|
+
response_data = await format_adapter.convert_response(
|
|
559
|
+
response_data
|
|
560
|
+
)
|
|
561
|
+
except Exception as exc:
|
|
562
|
+
logger.error(
|
|
563
|
+
"format_response_conversion_failed",
|
|
564
|
+
error=str(exc),
|
|
565
|
+
format_chain=format_chain,
|
|
566
|
+
endpoint=endpoint,
|
|
567
|
+
category="format",
|
|
568
|
+
exc_info=exc,
|
|
569
|
+
)
|
|
570
|
+
raise HTTPException(
|
|
571
|
+
status_code=500,
|
|
572
|
+
detail="Failed to convert Claude SDK response payload",
|
|
573
|
+
) from exc
|
|
574
|
+
|
|
575
|
+
return Response(
|
|
576
|
+
content=json.dumps(response_data),
|
|
577
|
+
media_type="application/json",
|
|
578
|
+
headers={
|
|
579
|
+
"X-Claude-SDK-Response": "true",
|
|
580
|
+
},
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
except httpx.TimeoutException as e:
|
|
584
|
+
logger.error(
|
|
585
|
+
"request_timeout",
|
|
586
|
+
error=str(e),
|
|
587
|
+
exc_info=e,
|
|
588
|
+
category="http",
|
|
589
|
+
)
|
|
590
|
+
raise HTTPException(status_code=408, detail="Request timed out") from e
|
|
591
|
+
except httpx.HTTPError as e:
|
|
592
|
+
logger.error(
|
|
593
|
+
"http_error",
|
|
594
|
+
error=str(e),
|
|
595
|
+
status_code=getattr(e.response, "status_code", None)
|
|
596
|
+
if hasattr(e, "response")
|
|
597
|
+
else None,
|
|
598
|
+
exc_info=e,
|
|
599
|
+
category="http",
|
|
600
|
+
)
|
|
601
|
+
raise HTTPException(status_code=502, detail=f"HTTP error: {e}") from e
|
|
602
|
+
except asyncio.CancelledError as e:
|
|
603
|
+
logger.warning(
|
|
604
|
+
"request_cancelled",
|
|
605
|
+
error=str(e),
|
|
606
|
+
exc_info=e,
|
|
607
|
+
)
|
|
608
|
+
raise
|
|
609
|
+
except Exception as e:
|
|
610
|
+
logger.error(
|
|
611
|
+
"request_handling_failed",
|
|
612
|
+
error=str(e),
|
|
613
|
+
exc_info=e,
|
|
614
|
+
)
|
|
615
|
+
raise HTTPException(
|
|
616
|
+
status_code=500, detail=f"SDK request failed: {str(e)}"
|
|
617
|
+
) from e
|
|
618
|
+
|
|
619
|
+
async def handle_streaming(
|
|
620
|
+
self, request: Request, endpoint: str, **kwargs: Any
|
|
621
|
+
) -> StreamingResponse:
|
|
622
|
+
"""Handle a streaming request through Claude SDK.
|
|
623
|
+
|
|
624
|
+
This is a convenience method that ensures stream=true and delegates
|
|
625
|
+
to handle_request which handles both streaming and non-streaming.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
request: FastAPI request object
|
|
629
|
+
endpoint: Target endpoint path
|
|
630
|
+
**kwargs: Additional arguments
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
Streaming response from Claude SDK
|
|
634
|
+
"""
|
|
635
|
+
if not self._initialized:
|
|
636
|
+
await self.initialize()
|
|
637
|
+
|
|
638
|
+
# Parse and modify request to ensure stream=true
|
|
639
|
+
body = await request.body()
|
|
640
|
+
if not body:
|
|
641
|
+
request_data = {"stream": True}
|
|
642
|
+
else:
|
|
643
|
+
try:
|
|
644
|
+
request_data = json.loads(body)
|
|
645
|
+
except json.JSONDecodeError:
|
|
646
|
+
request_data = {"stream": True}
|
|
647
|
+
|
|
648
|
+
# Force streaming
|
|
649
|
+
request_data["stream"] = True
|
|
650
|
+
modified_body = json.dumps(request_data).encode()
|
|
651
|
+
|
|
652
|
+
# Create modified request with stream=true
|
|
653
|
+
modified_scope = {
|
|
654
|
+
**request.scope,
|
|
655
|
+
"_body": modified_body,
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
modified_request = StarletteRequest(
|
|
659
|
+
scope=modified_scope,
|
|
660
|
+
receive=request.receive,
|
|
661
|
+
)
|
|
662
|
+
modified_request._body = modified_body
|
|
663
|
+
|
|
664
|
+
# Delegate to handle_request which will handle streaming
|
|
665
|
+
result = await self.handle_request(modified_request)
|
|
666
|
+
|
|
667
|
+
# Ensure we return a streaming response
|
|
668
|
+
if not isinstance(result, StreamingResponse):
|
|
669
|
+
# This shouldn't happen since we forced stream=true, but handle it gracefully
|
|
670
|
+
logger.warning(
|
|
671
|
+
"unexpected_response_type",
|
|
672
|
+
expected="StreamingResponse",
|
|
673
|
+
actual=type(result).__name__,
|
|
674
|
+
)
|
|
675
|
+
return StreamingResponse(
|
|
676
|
+
iter([result.body if hasattr(result, "body") else b""]),
|
|
677
|
+
media_type="text/event-stream",
|
|
678
|
+
headers={"X-Claude-SDK-Response": "true"},
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
return result
|
|
682
|
+
|
|
683
|
+
async def cleanup(self) -> None:
|
|
684
|
+
"""Cleanup resources when shutting down."""
|
|
685
|
+
try:
|
|
686
|
+
# Shutdown session manager first
|
|
687
|
+
if self.session_manager:
|
|
688
|
+
await self.session_manager.shutdown()
|
|
689
|
+
self.session_manager = None
|
|
690
|
+
|
|
691
|
+
# Close handler
|
|
692
|
+
if self.handler:
|
|
693
|
+
await self.handler.close()
|
|
694
|
+
self.handler = None
|
|
695
|
+
|
|
696
|
+
# Clear references to prevent memory leaks
|
|
697
|
+
self._detection_service = None
|
|
698
|
+
|
|
699
|
+
# Mark as not initialized
|
|
700
|
+
self._initialized = False
|
|
701
|
+
|
|
702
|
+
logger.debug("adapter_cleanup_completed")
|
|
703
|
+
|
|
704
|
+
except Exception as e:
|
|
705
|
+
logger.error(
|
|
706
|
+
"adapter_cleanup_failed",
|
|
707
|
+
error=str(e),
|
|
708
|
+
exc_info=e,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
async def close(self) -> None:
|
|
712
|
+
"""Compatibility method - delegates to cleanup()."""
|
|
713
|
+
await self.cleanup()
|
|
714
|
+
|
|
715
|
+
# BaseHTTPAdapter abstract method implementations
|
|
716
|
+
# Note: ClaudeSDK doesn't use external HTTP, so these methods are minimal implementations
|
|
717
|
+
|
|
718
|
+
async def prepare_provider_request(
|
|
719
|
+
self, body: bytes, headers: dict[str, str], endpoint: str
|
|
720
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
721
|
+
"""Prepare request for ClaudeSDK (minimal implementation).
|
|
722
|
+
|
|
723
|
+
ClaudeSDK uses the local Claude SDK rather than making HTTP requests,
|
|
724
|
+
so this just passes through the body and headers.
|
|
725
|
+
"""
|
|
726
|
+
return body, headers
|
|
727
|
+
|
|
728
|
+
async def process_provider_response(
|
|
729
|
+
self, response: "httpx.Response", endpoint: str
|
|
730
|
+
) -> Response | StreamingResponse:
|
|
731
|
+
"""Process response from ClaudeSDK (minimal implementation).
|
|
732
|
+
|
|
733
|
+
ClaudeSDK handles response processing in handle_request method,
|
|
734
|
+
so this should not be called in normal operation.
|
|
735
|
+
"""
|
|
736
|
+
# This shouldn't be called for ClaudeSDK, but provide a fallback
|
|
737
|
+
return Response(
|
|
738
|
+
content=response.content,
|
|
739
|
+
status_code=response.status_code,
|
|
740
|
+
headers=dict(response.headers),
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
async def get_target_url(self, endpoint: str) -> str:
|
|
744
|
+
"""Get target URL for ClaudeSDK (minimal implementation).
|
|
745
|
+
|
|
746
|
+
ClaudeSDK uses local SDK rather than HTTP URLs,
|
|
747
|
+
so this returns a placeholder URL.
|
|
748
|
+
"""
|
|
749
|
+
return f"claude-sdk://local/{endpoint.lstrip('/')}"
|