ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Streaming request handler for SSE and chunked responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from ccproxy.core.plugins.hooks import HookManager
|
|
12
|
+
from ccproxy.core.request_context import RequestContext
|
|
13
|
+
from ccproxy.services.handler_config import HandlerConfig
|
|
14
|
+
from ccproxy.streaming.deferred import DeferredStreaming
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = structlog.get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StreamingHandler:
|
|
21
|
+
"""Manages streaming request processing with header preservation and SSE adaptation."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
hook_manager: HookManager | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize with hook manager for stream events.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
hook_manager: Optional hook manager for emitting stream events
|
|
31
|
+
"""
|
|
32
|
+
self.hook_manager = hook_manager
|
|
33
|
+
|
|
34
|
+
def should_stream_response(self, headers: dict[str, str]) -> bool:
|
|
35
|
+
"""Detect streaming intent from request headers.
|
|
36
|
+
|
|
37
|
+
- Prefer client `Accept: text/event-stream`
|
|
38
|
+
- Fallback to provider-style `Content-Type: text/event-stream` (rare for requests)
|
|
39
|
+
- Case-insensitive checks
|
|
40
|
+
"""
|
|
41
|
+
accept = str(headers.get("accept", "")).lower()
|
|
42
|
+
if "text/event-stream" in accept:
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
content_type = str(headers.get("content-type", "")).lower()
|
|
46
|
+
return "text/event-stream" in content_type
|
|
47
|
+
|
|
48
|
+
async def should_stream(
|
|
49
|
+
self, request_body: bytes, handler_config: HandlerConfig
|
|
50
|
+
) -> bool:
|
|
51
|
+
"""Check if request body has stream:true flag.
|
|
52
|
+
|
|
53
|
+
- Returns False if provider doesn't support streaming
|
|
54
|
+
- Parses JSON body for 'stream' field
|
|
55
|
+
- Handles parse errors gracefully
|
|
56
|
+
"""
|
|
57
|
+
if not handler_config.supports_streaming:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
data = json.loads(request_body)
|
|
62
|
+
return data.get("stream", False) is True
|
|
63
|
+
except (json.JSONDecodeError, TypeError):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
async def handle_streaming_request(
|
|
67
|
+
self,
|
|
68
|
+
method: str,
|
|
69
|
+
url: str,
|
|
70
|
+
headers: dict[str, str],
|
|
71
|
+
body: bytes,
|
|
72
|
+
handler_config: HandlerConfig,
|
|
73
|
+
request_context: RequestContext,
|
|
74
|
+
on_headers: Any | None = None,
|
|
75
|
+
client_config: dict[str, Any] | None = None,
|
|
76
|
+
client: httpx.AsyncClient | None = None,
|
|
77
|
+
) -> DeferredStreaming:
|
|
78
|
+
"""Create a deferred streaming response that preserves headers.
|
|
79
|
+
|
|
80
|
+
This always returns a DeferredStreaming response which:
|
|
81
|
+
- Defers the actual HTTP request until FastAPI sends the response
|
|
82
|
+
- Captures all upstream headers correctly
|
|
83
|
+
- Supports SSE processing through handler_config
|
|
84
|
+
- Provides request tracing and metrics
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
# Use provided client or create a short-lived one
|
|
88
|
+
owns_client = False
|
|
89
|
+
if client is None:
|
|
90
|
+
client = httpx.AsyncClient(**(client_config or {}))
|
|
91
|
+
owns_client = True
|
|
92
|
+
|
|
93
|
+
# Log that we're creating a deferred response
|
|
94
|
+
logger.debug(
|
|
95
|
+
"streaming_handler_creating_deferred_response",
|
|
96
|
+
url=url,
|
|
97
|
+
method=method,
|
|
98
|
+
has_sse_adapter=bool(handler_config.response_adapter),
|
|
99
|
+
adapter_type=type(handler_config.response_adapter).__name__
|
|
100
|
+
if handler_config.response_adapter
|
|
101
|
+
else None,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Return the deferred response with format adapter from handler config
|
|
105
|
+
return DeferredStreaming(
|
|
106
|
+
method=method,
|
|
107
|
+
url=url,
|
|
108
|
+
headers=headers,
|
|
109
|
+
body=body,
|
|
110
|
+
client=client,
|
|
111
|
+
media_type="text/event-stream; charset=utf-8",
|
|
112
|
+
handler_config=handler_config, # Contains format adapter if needed
|
|
113
|
+
request_context=request_context,
|
|
114
|
+
hook_manager=self.hook_manager,
|
|
115
|
+
on_headers=on_headers,
|
|
116
|
+
close_client_on_finish=owns_client,
|
|
117
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Streaming interfaces for provider implementations.
|
|
2
|
+
|
|
3
|
+
This module defines interfaces that providers can implement to extend
|
|
4
|
+
streaming functionality without coupling core code to specific providers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from typing_extensions import TypedDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StreamingMetrics(TypedDict, total=False):
|
|
13
|
+
"""Standard streaming metrics structure."""
|
|
14
|
+
|
|
15
|
+
tokens_input: int | None
|
|
16
|
+
tokens_output: int | None
|
|
17
|
+
cache_read_tokens: int | None
|
|
18
|
+
cache_write_tokens: int | None
|
|
19
|
+
cost_usd: float | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IStreamingMetricsCollector(Protocol):
|
|
23
|
+
"""Interface for provider-specific streaming metrics collection.
|
|
24
|
+
|
|
25
|
+
Providers implement this interface to extract token usage and other
|
|
26
|
+
metrics from their specific streaming response formats.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def process_chunk(self, chunk_str: str) -> bool:
|
|
30
|
+
"""Process a streaming chunk to extract metrics.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
chunk_str: Raw chunk string from streaming response
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if this was the final chunk with complete metrics, False otherwise
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def process_raw_chunk(self, chunk_str: str) -> bool:
|
|
41
|
+
"""Process a raw provider chunk before any format conversion.
|
|
42
|
+
|
|
43
|
+
This method is called with chunks in the provider's native format,
|
|
44
|
+
before any OpenAI/Anthropic format conversion happens.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
chunk_str: Raw chunk string in provider's native format
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if this was the final chunk with complete metrics, False otherwise
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def process_converted_chunk(self, chunk_str: str) -> bool:
|
|
55
|
+
"""Process a chunk after format conversion.
|
|
56
|
+
|
|
57
|
+
This method is called with chunks after they've been converted
|
|
58
|
+
to a different format (e.g., OpenAI format).
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
chunk_str: Chunk string after format conversion
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if this was the final chunk with complete metrics, False otherwise
|
|
65
|
+
"""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def get_metrics(self) -> StreamingMetrics:
|
|
69
|
+
"""Get the collected metrics.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dictionary with provider-specific metrics (tokens, costs, etc.)
|
|
73
|
+
"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Moved StreamingConfigurable to ccproxy.core.interfaces to avoid circular imports
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Simplified streaming adapter that bypasses complex type conversions.
|
|
2
|
+
|
|
3
|
+
This adapter provides a direct dict-based interface for streaming without
|
|
4
|
+
the complexity of the shim layer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SimpleStreamingAdapter:
|
|
12
|
+
"""Simple adapter for streaming responses that works directly with dicts."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str = "simple_streaming"):
|
|
15
|
+
"""Initialize the simple adapter."""
|
|
16
|
+
self.name = name
|
|
17
|
+
|
|
18
|
+
async def adapt_request(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
"""Pass through request - no adaptation needed for streaming."""
|
|
20
|
+
return request
|
|
21
|
+
|
|
22
|
+
async def adapt_response(self, response: dict[str, Any]) -> dict[str, Any]:
|
|
23
|
+
"""Pass through response - no adaptation needed for streaming."""
|
|
24
|
+
return response
|
|
25
|
+
|
|
26
|
+
def adapt_stream(
|
|
27
|
+
self, stream: AsyncIterator[dict[str, Any]]
|
|
28
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
29
|
+
"""Pass through stream - no adaptation needed for simple streaming."""
|
|
30
|
+
|
|
31
|
+
async def passthrough_stream() -> AsyncGenerator[dict[str, Any], None]:
|
|
32
|
+
async for chunk in stream:
|
|
33
|
+
yield chunk
|
|
34
|
+
|
|
35
|
+
return passthrough_stream()
|
|
36
|
+
|
|
37
|
+
async def adapt_error(self, error: dict[str, Any]) -> dict[str, Any]:
|
|
38
|
+
"""Pass through error - no adaptation needed."""
|
|
39
|
+
return error
|
ccproxy/streaming/sse.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Shared helpers for serializing JSON streams into SSE messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ccproxy.core.logging import get_logger
|
|
10
|
+
from ccproxy.llms.streaming import AnthropicSSEFormatter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def serialize_json_to_sse_stream(
|
|
17
|
+
json_stream: AsyncIterator[Any],
|
|
18
|
+
*,
|
|
19
|
+
include_done: bool = True,
|
|
20
|
+
request_context: Any | None = None,
|
|
21
|
+
) -> AsyncGenerator[bytes, None]:
|
|
22
|
+
"""Serialize JSON-like stream items into SSE-compliant bytes.
|
|
23
|
+
|
|
24
|
+
This matches the behaviour previously implemented in
|
|
25
|
+
``DeferredStreaming._serialize_json_to_sse_stream`` and is shared by
|
|
26
|
+
SDK and HTTP-based providers alike.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
json_stream: Async iterator yielding dict-like SSE payloads (or
|
|
30
|
+
objects with ``model_dump``/``dict``).
|
|
31
|
+
include_done: Whether to append the ``data: [DONE]`` sentinel at
|
|
32
|
+
the end of the stream.
|
|
33
|
+
request_context: Optional request context for logging (expects a
|
|
34
|
+
``request_id`` attribute when present).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
formatter = AnthropicSSEFormatter()
|
|
38
|
+
request_id = None
|
|
39
|
+
if request_context and hasattr(request_context, "request_id"):
|
|
40
|
+
request_id = request_context.request_id
|
|
41
|
+
|
|
42
|
+
chunk_count = 0
|
|
43
|
+
anthropic_chunks = 0
|
|
44
|
+
openai_chunks = 0
|
|
45
|
+
|
|
46
|
+
async for json_obj in json_stream:
|
|
47
|
+
chunk_count += 1
|
|
48
|
+
|
|
49
|
+
# Normalise the payload to a dict
|
|
50
|
+
if hasattr(json_obj, "model_dump") and callable(json_obj.model_dump):
|
|
51
|
+
json_obj = json_obj.model_dump()
|
|
52
|
+
elif hasattr(json_obj, "dict") and callable(json_obj.dict):
|
|
53
|
+
json_obj = json_obj.dict()
|
|
54
|
+
|
|
55
|
+
if not isinstance(json_obj, dict):
|
|
56
|
+
# Skip unsupported payloads
|
|
57
|
+
logger.debug(
|
|
58
|
+
"sse_serialization_skipped_non_dict",
|
|
59
|
+
chunk_number=chunk_count,
|
|
60
|
+
payload_type=type(json_obj).__name__,
|
|
61
|
+
request_id=request_id,
|
|
62
|
+
category="sse_format",
|
|
63
|
+
)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
event_type = json_obj.get("type")
|
|
67
|
+
if isinstance(event_type, str) and event_type:
|
|
68
|
+
anthropic_chunks += 1
|
|
69
|
+
if event_type == "ping":
|
|
70
|
+
sse_event = formatter.format_ping()
|
|
71
|
+
else:
|
|
72
|
+
sse_event = formatter.format_event(event_type, json_obj)
|
|
73
|
+
|
|
74
|
+
logger.trace(
|
|
75
|
+
"sse_serialization_anthropic_format",
|
|
76
|
+
event_type=event_type,
|
|
77
|
+
chunk_number=chunk_count,
|
|
78
|
+
request_id=request_id,
|
|
79
|
+
category="sse_format",
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
openai_chunks += 1
|
|
83
|
+
json_str = json.dumps(json_obj, ensure_ascii=False)
|
|
84
|
+
sse_event = f"data: {json_str}\n\n"
|
|
85
|
+
|
|
86
|
+
logger.trace(
|
|
87
|
+
"sse_serialization_openai_format",
|
|
88
|
+
chunk_number=chunk_count,
|
|
89
|
+
has_choices=bool(json_obj.get("choices")),
|
|
90
|
+
request_id=request_id,
|
|
91
|
+
category="sse_format",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
yield sse_event.encode("utf-8")
|
|
95
|
+
|
|
96
|
+
logger.debug(
|
|
97
|
+
"sse_serialization_complete",
|
|
98
|
+
total_chunks=chunk_count,
|
|
99
|
+
anthropic_chunks=anthropic_chunks,
|
|
100
|
+
openai_chunks=openai_chunks,
|
|
101
|
+
request_id=request_id,
|
|
102
|
+
category="sse_format",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if include_done:
|
|
106
|
+
yield b"data: [DONE]\n\n"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = ["serialize_json_to_sse_stream"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Helpers for incrementally parsing server-sent events (SSE)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SSEStreamParser:
|
|
10
|
+
"""Accumulate SSE fragments and yield decoded ``data:`` payloads.
|
|
11
|
+
|
|
12
|
+
The parser keeps track of partial lines and events across ``feed`` calls so
|
|
13
|
+
callers can push raw provider chunks (``str`` or ``bytes``) and only receive
|
|
14
|
+
payloads when a full SSE event has been received. ``data: [DONE]`` sentinel
|
|
15
|
+
events are filtered out automatically.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__slots__ = ("_line_remainder", "_event_lines", "_errors")
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._line_remainder: str = ""
|
|
22
|
+
self._event_lines: list[str] = []
|
|
23
|
+
self._errors: list[tuple[str, Exception]] = []
|
|
24
|
+
|
|
25
|
+
def feed(self, chunk: str | bytes | None) -> list[Any]:
|
|
26
|
+
"""Process a streaming fragment and return decoded JSON payloads.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
chunk: Raw chunk from the provider. ``bytes`` inputs are decoded
|
|
30
|
+
using UTF-8. ``None`` or empty values yield no events.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of decoded JSON payloads for completed events. ``[DONE]``
|
|
34
|
+
sentinels are omitted.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if not chunk:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
if isinstance(chunk, bytes):
|
|
41
|
+
chunk = chunk.decode("utf-8", errors="ignore")
|
|
42
|
+
|
|
43
|
+
if not chunk:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
chunk = chunk.replace("\r\n", "\n").replace("\r", "\n")
|
|
47
|
+
buffered = f"{self._line_remainder}{chunk}"
|
|
48
|
+
|
|
49
|
+
lines = buffered.split("\n")
|
|
50
|
+
if buffered.endswith("\n"):
|
|
51
|
+
self._line_remainder = ""
|
|
52
|
+
else:
|
|
53
|
+
self._line_remainder = lines.pop()
|
|
54
|
+
|
|
55
|
+
completed: list[Any] = []
|
|
56
|
+
|
|
57
|
+
for line in lines:
|
|
58
|
+
if line == "":
|
|
59
|
+
payload = self._finalize_event()
|
|
60
|
+
if payload:
|
|
61
|
+
completed.append(payload)
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
self._event_lines.append(line)
|
|
65
|
+
|
|
66
|
+
return completed
|
|
67
|
+
|
|
68
|
+
def flush(self) -> list[Any]:
|
|
69
|
+
"""Return any buffered payload when the stream ends."""
|
|
70
|
+
|
|
71
|
+
if self._line_remainder:
|
|
72
|
+
self._event_lines.append(self._line_remainder)
|
|
73
|
+
self._line_remainder = ""
|
|
74
|
+
|
|
75
|
+
payload = self._finalize_event()
|
|
76
|
+
return [payload] if payload else []
|
|
77
|
+
|
|
78
|
+
def consume_errors(self) -> list[tuple[str, Exception]]:
|
|
79
|
+
"""Return and clear parsing errors captured since the last call."""
|
|
80
|
+
|
|
81
|
+
errors = self._errors
|
|
82
|
+
self._errors = []
|
|
83
|
+
return errors
|
|
84
|
+
|
|
85
|
+
def _finalize_event(self) -> Any | None:
|
|
86
|
+
if not self._event_lines:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
event_lines, self._event_lines = self._event_lines, []
|
|
90
|
+
|
|
91
|
+
data_fields: list[str] = []
|
|
92
|
+
for line in event_lines:
|
|
93
|
+
if line.startswith("data:"):
|
|
94
|
+
data_fields.append(line[5:].lstrip(" "))
|
|
95
|
+
|
|
96
|
+
if not data_fields:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# Try newline-preserving join first (Anthropic style), then a collapsed
|
|
100
|
+
# join for providers that stream JSON without explicit newlines.
|
|
101
|
+
candidates = ["\n".join(data_fields).strip()]
|
|
102
|
+
collapsed = "".join(data_fields).strip()
|
|
103
|
+
if collapsed and collapsed != candidates[0]:
|
|
104
|
+
candidates.append(collapsed)
|
|
105
|
+
|
|
106
|
+
last_exception: json.JSONDecodeError | None = None
|
|
107
|
+
|
|
108
|
+
for candidate in candidates:
|
|
109
|
+
if not candidate or candidate == "[DONE]":
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
return json.loads(candidate)
|
|
114
|
+
except json.JSONDecodeError as exc:
|
|
115
|
+
last_exception = exc
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if last_exception:
|
|
119
|
+
# Reconstruct the raw event so we can retry when more data arrives.
|
|
120
|
+
raw_event = "\n".join(event_lines) + "\n\n"
|
|
121
|
+
self._line_remainder = f"{raw_event}{self._line_remainder}"
|
|
122
|
+
self._errors.append((raw_event, last_exception))
|
|
123
|
+
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = ["SSEStreamParser"]
|