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,76 @@
|
|
|
1
|
+
"""Handler configuration for request handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from ccproxy.services.adapters.format_context import FormatContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ccproxy.services.adapters.format_adapter import FormatAdapterProtocol
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class PluginTransformerProtocol(Protocol):
|
|
17
|
+
"""Protocol for plugin-based transformers with header and body methods."""
|
|
18
|
+
|
|
19
|
+
def transform_headers(
|
|
20
|
+
self, headers: dict[str, str], *args: Any, **kwargs: Any
|
|
21
|
+
) -> dict[str, str]:
|
|
22
|
+
"""Transform request headers."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class SSEParserProtocol(Protocol):
|
|
28
|
+
"""Protocol for SSE parsers to extract a final JSON response.
|
|
29
|
+
|
|
30
|
+
Implementations should return a parsed dict for the final response, or
|
|
31
|
+
None if no final response could be determined.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __call__(
|
|
35
|
+
self, raw: str
|
|
36
|
+
) -> dict[str, Any] | None: # pragma: no cover - protocol
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def transform_body(self, body: Any) -> Any:
|
|
40
|
+
"""Transform request body."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class HandlerConfig:
|
|
46
|
+
"""Processing pipeline configuration for HTTP/streaming handlers.
|
|
47
|
+
|
|
48
|
+
This config only contains universal processing concerns,
|
|
49
|
+
not plugin-specific parameters like session_id or access_token.
|
|
50
|
+
|
|
51
|
+
Following the Parameter Object pattern, this groups related processing
|
|
52
|
+
components while maintaining clean separation of concerns. Plugin-specific
|
|
53
|
+
parameters should be passed directly as method parameters.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Format conversion (e.g., OpenAI ↔ Anthropic)
|
|
57
|
+
request_adapter: FormatAdapterProtocol | None = None
|
|
58
|
+
response_adapter: FormatAdapterProtocol | None = None
|
|
59
|
+
|
|
60
|
+
# Header/body transformation
|
|
61
|
+
request_transformer: PluginTransformerProtocol | None = None
|
|
62
|
+
response_transformer: PluginTransformerProtocol | None = None
|
|
63
|
+
|
|
64
|
+
# Feature flag
|
|
65
|
+
supports_streaming: bool = True
|
|
66
|
+
|
|
67
|
+
# Header case preservation toggle for upstream requests
|
|
68
|
+
# When True, the HTTP handler will not canonicalize header names and will
|
|
69
|
+
# forward them with their original casing/order as produced by transformers.
|
|
70
|
+
preserve_header_case: bool = False
|
|
71
|
+
|
|
72
|
+
# Optional SSE parser provided by plugins that return SSE streams
|
|
73
|
+
sse_parser: SSEParserProtocol | None = None
|
|
74
|
+
|
|
75
|
+
# Format context for adapter selection
|
|
76
|
+
format_context: FormatContext | None = None
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Service interfaces for explicit dependency injection.
|
|
2
|
+
|
|
3
|
+
This module defines protocol interfaces for core services that adapters need,
|
|
4
|
+
enabling explicit dependency injection and removing the service locator pattern.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from starlette.responses import Response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ccproxy.core.request_context import RequestContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IRequestHandler(Protocol):
|
|
19
|
+
"""Protocol for request handling functionality.
|
|
20
|
+
|
|
21
|
+
Note: The dispatch_request method has been removed in favor of
|
|
22
|
+
using plugin adapters' handle_request() method directly.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IRequestTracer(Protocol):
|
|
29
|
+
"""Request tracing interface."""
|
|
30
|
+
|
|
31
|
+
async def trace_request(
|
|
32
|
+
self,
|
|
33
|
+
request_id: str,
|
|
34
|
+
method: str,
|
|
35
|
+
url: str,
|
|
36
|
+
headers: dict[str, str],
|
|
37
|
+
body: bytes | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Trace an outgoing request.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
request_id: Unique request identifier
|
|
43
|
+
method: HTTP method
|
|
44
|
+
url: Target URL
|
|
45
|
+
headers: Request headers
|
|
46
|
+
body: Request body if available
|
|
47
|
+
"""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
async def trace_response(
|
|
51
|
+
self,
|
|
52
|
+
request_id: str,
|
|
53
|
+
status: int,
|
|
54
|
+
headers: dict[str, str],
|
|
55
|
+
body: bytes | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Trace an incoming response.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
request_id: Unique request identifier
|
|
61
|
+
status: HTTP status code
|
|
62
|
+
headers: Response headers
|
|
63
|
+
body: Response body if available
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def should_trace(self) -> bool:
|
|
68
|
+
"""Check if tracing is enabled.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if tracing should be performed
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class IMetricsCollector(Protocol):
|
|
77
|
+
"""Metrics collection interface."""
|
|
78
|
+
|
|
79
|
+
def track_request(
|
|
80
|
+
self, method: str, path: str, provider: str | None = None
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Track an incoming request.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
method: HTTP method
|
|
86
|
+
path: Request path
|
|
87
|
+
provider: Optional provider identifier
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def track_response(
|
|
92
|
+
self, status: int, duration: float, provider: str | None = None
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Track a response.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
status: HTTP status code
|
|
98
|
+
duration: Response time in seconds
|
|
99
|
+
provider: Optional provider identifier
|
|
100
|
+
"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def track_error(self, error_type: str, provider: str | None = None) -> None:
|
|
104
|
+
"""Track an error occurrence.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
error_type: Type of error
|
|
108
|
+
provider: Optional provider identifier
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
def track_tokens(
|
|
113
|
+
self,
|
|
114
|
+
input_tokens: int,
|
|
115
|
+
output_tokens: int,
|
|
116
|
+
provider: str | None = None,
|
|
117
|
+
model: str | None = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Track token usage.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
input_tokens: Number of input tokens
|
|
123
|
+
output_tokens: Number of output tokens
|
|
124
|
+
provider: Optional provider identifier
|
|
125
|
+
model: Optional model identifier
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class StreamingMetrics(Protocol):
|
|
131
|
+
"""Streaming response handler interface."""
|
|
132
|
+
|
|
133
|
+
async def handle_stream(
|
|
134
|
+
self,
|
|
135
|
+
response: httpx.Response,
|
|
136
|
+
request_context: "RequestContext | None" = None,
|
|
137
|
+
) -> AsyncIterator[bytes]:
|
|
138
|
+
"""Handle a streaming response.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
response: HTTP response object
|
|
142
|
+
request_context: Optional request context
|
|
143
|
+
|
|
144
|
+
Yields:
|
|
145
|
+
Response chunks
|
|
146
|
+
"""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
def create_streaming_response(
|
|
150
|
+
self,
|
|
151
|
+
stream: AsyncIterator[bytes],
|
|
152
|
+
headers: dict[str, str] | None = None,
|
|
153
|
+
) -> Response:
|
|
154
|
+
"""Create a streaming response.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
stream: Async iterator of response chunks
|
|
158
|
+
headers: Optional response headers
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Streaming response object
|
|
162
|
+
"""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
async def handle_streaming_request(
|
|
166
|
+
self,
|
|
167
|
+
method: str,
|
|
168
|
+
url: str,
|
|
169
|
+
headers: dict[str, str],
|
|
170
|
+
body: bytes,
|
|
171
|
+
handler_config: Any,
|
|
172
|
+
request_context: Any,
|
|
173
|
+
client_config: dict[str, Any] | None = None,
|
|
174
|
+
client: httpx.AsyncClient | None = None,
|
|
175
|
+
) -> Any:
|
|
176
|
+
"""Handle a streaming request.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
method: HTTP method
|
|
180
|
+
url: Target URL
|
|
181
|
+
headers: Request headers
|
|
182
|
+
body: Request body
|
|
183
|
+
handler_config: Handler configuration
|
|
184
|
+
request_context: Request context
|
|
185
|
+
client_config: Optional client configuration
|
|
186
|
+
client: Optional HTTP client
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Deferred streaming response
|
|
190
|
+
"""
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Null implementations for optional dependencies
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class NullRequestTracer:
|
|
198
|
+
"""Null implementation of request tracer (no-op)."""
|
|
199
|
+
|
|
200
|
+
async def trace_request(
|
|
201
|
+
self,
|
|
202
|
+
request_id: str,
|
|
203
|
+
method: str,
|
|
204
|
+
url: str,
|
|
205
|
+
headers: dict[str, str],
|
|
206
|
+
body: bytes | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""No-op request tracing."""
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
async def trace_response(
|
|
212
|
+
self,
|
|
213
|
+
request_id: str,
|
|
214
|
+
status: int,
|
|
215
|
+
headers: dict[str, str],
|
|
216
|
+
body: bytes | None = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""No-op response tracing."""
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
def should_trace(self) -> bool:
|
|
222
|
+
"""Always return False for null tracer."""
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class NullMetricsCollector:
|
|
227
|
+
"""Null implementation of metrics collector (no-op)."""
|
|
228
|
+
|
|
229
|
+
def track_request(
|
|
230
|
+
self, method: str, path: str, provider: str | None = None
|
|
231
|
+
) -> None:
|
|
232
|
+
"""No-op request tracking."""
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
def track_response(
|
|
236
|
+
self, status: int, duration: float, provider: str | None = None
|
|
237
|
+
) -> None:
|
|
238
|
+
"""No-op response tracking."""
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
def track_error(self, error_type: str, provider: str | None = None) -> None:
|
|
242
|
+
"""No-op error tracking."""
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
def track_tokens(
|
|
246
|
+
self,
|
|
247
|
+
input_tokens: int,
|
|
248
|
+
output_tokens: int,
|
|
249
|
+
provider: str | None = None,
|
|
250
|
+
model: str | None = None,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""No-op token tracking."""
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class NullStreamingHandler:
|
|
257
|
+
"""Null implementation of streaming handler."""
|
|
258
|
+
|
|
259
|
+
async def handle_stream(
|
|
260
|
+
self,
|
|
261
|
+
response: httpx.Response,
|
|
262
|
+
request_context: "RequestContext | None" = None,
|
|
263
|
+
) -> AsyncIterator[bytes]:
|
|
264
|
+
"""Return empty stream."""
|
|
265
|
+
# Make this a proper async generator
|
|
266
|
+
for _ in []:
|
|
267
|
+
yield b""
|
|
268
|
+
|
|
269
|
+
def create_streaming_response(
|
|
270
|
+
self,
|
|
271
|
+
stream: AsyncIterator[bytes],
|
|
272
|
+
headers: dict[str, str] | None = None,
|
|
273
|
+
) -> Response:
|
|
274
|
+
"""Create empty response."""
|
|
275
|
+
from starlette.responses import Response
|
|
276
|
+
|
|
277
|
+
return Response(content=b"", headers=headers or {})
|
|
278
|
+
|
|
279
|
+
async def handle_streaming_request(
|
|
280
|
+
self,
|
|
281
|
+
method: str,
|
|
282
|
+
url: str,
|
|
283
|
+
headers: dict[str, str],
|
|
284
|
+
body: bytes,
|
|
285
|
+
handler_config: Any,
|
|
286
|
+
request_context: Any,
|
|
287
|
+
client_config: dict[str, Any] | None = None,
|
|
288
|
+
client: httpx.AsyncClient | None = None,
|
|
289
|
+
) -> Any:
|
|
290
|
+
"""Null implementation - returns a simple error response."""
|
|
291
|
+
# For null implementation, return a regular response instead of trying to stream
|
|
292
|
+
from starlette.responses import JSONResponse
|
|
293
|
+
|
|
294
|
+
return JSONResponse(
|
|
295
|
+
content={"error": "Streaming handler not available"},
|
|
296
|
+
status_code=503, # Service Unavailable
|
|
297
|
+
headers={"X-Error": "NullStreamingHandler"},
|
|
298
|
+
)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Mock response handler for bypass mode."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import random
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
from fastapi.responses import StreamingResponse
|
|
11
|
+
|
|
12
|
+
from ccproxy.core.request_context import RequestContext
|
|
13
|
+
from ccproxy.services.adapters.format_adapter import DictFormatAdapter
|
|
14
|
+
from ccproxy.services.adapters.simple_converters import (
|
|
15
|
+
convert_anthropic_to_openai_response,
|
|
16
|
+
)
|
|
17
|
+
from ccproxy.testing import RealisticMockResponseGenerator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = structlog.get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MockResponseHandler:
|
|
24
|
+
"""Handles bypass mode with realistic mock responses."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
mock_generator: RealisticMockResponseGenerator,
|
|
29
|
+
openai_adapter: DictFormatAdapter | None = None,
|
|
30
|
+
error_rate: float = 0.05,
|
|
31
|
+
latency_range: tuple[float, float] = (0.5, 2.0),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Initialize with mock generator and format adapter.
|
|
34
|
+
|
|
35
|
+
- Uses existing testing utilities
|
|
36
|
+
- Supports both Anthropic and OpenAI formats
|
|
37
|
+
"""
|
|
38
|
+
self.mock_generator = mock_generator
|
|
39
|
+
if openai_adapter is None:
|
|
40
|
+
openai_adapter = DictFormatAdapter(
|
|
41
|
+
response=convert_anthropic_to_openai_response,
|
|
42
|
+
name="mock_anthropic_to_openai",
|
|
43
|
+
)
|
|
44
|
+
self.openai_adapter = openai_adapter
|
|
45
|
+
self.error_rate = error_rate
|
|
46
|
+
self.latency_range = latency_range
|
|
47
|
+
|
|
48
|
+
def extract_message_type(self, body: bytes | None) -> str:
|
|
49
|
+
"""Analyze request body to determine response type.
|
|
50
|
+
|
|
51
|
+
- Checks for 'tools' field → returns 'tool_use'
|
|
52
|
+
- Analyzes message length → returns 'long'|'medium'|'short'
|
|
53
|
+
- Handles JSON decode errors gracefully
|
|
54
|
+
"""
|
|
55
|
+
if not body:
|
|
56
|
+
return "short"
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
data = json.loads(body)
|
|
60
|
+
|
|
61
|
+
# Check for tool use
|
|
62
|
+
if "tools" in data:
|
|
63
|
+
return "tool_use"
|
|
64
|
+
|
|
65
|
+
# Analyze message content length
|
|
66
|
+
messages = data.get("messages", [])
|
|
67
|
+
if messages:
|
|
68
|
+
total_content_length = sum(
|
|
69
|
+
len(msg.get("content", ""))
|
|
70
|
+
for msg in messages
|
|
71
|
+
if isinstance(msg.get("content"), str)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if total_content_length > 1000:
|
|
75
|
+
return "long"
|
|
76
|
+
elif total_content_length > 200:
|
|
77
|
+
return "medium"
|
|
78
|
+
|
|
79
|
+
return "short"
|
|
80
|
+
|
|
81
|
+
except (json.JSONDecodeError, TypeError):
|
|
82
|
+
return "short"
|
|
83
|
+
|
|
84
|
+
def should_simulate_error(self) -> bool:
|
|
85
|
+
"""Randomly decide if error should be simulated.
|
|
86
|
+
|
|
87
|
+
- Uses configuration-based error rate
|
|
88
|
+
- Provides realistic error distribution
|
|
89
|
+
"""
|
|
90
|
+
return random.random() < self.error_rate
|
|
91
|
+
|
|
92
|
+
async def generate_standard_response(
|
|
93
|
+
self,
|
|
94
|
+
model: str | None,
|
|
95
|
+
is_openai_format: bool,
|
|
96
|
+
ctx: RequestContext,
|
|
97
|
+
message_type: str = "short",
|
|
98
|
+
) -> tuple[int, dict[str, str], bytes]:
|
|
99
|
+
"""Generate non-streaming mock response.
|
|
100
|
+
|
|
101
|
+
- Simulates realistic latency (configurable)
|
|
102
|
+
- Generates appropriate token counts
|
|
103
|
+
- Updates request context with metrics
|
|
104
|
+
- Returns (status_code, headers, body)
|
|
105
|
+
"""
|
|
106
|
+
# Simulate latency
|
|
107
|
+
latency = random.uniform(*self.latency_range)
|
|
108
|
+
await asyncio.sleep(latency)
|
|
109
|
+
|
|
110
|
+
# Check if we should simulate an error
|
|
111
|
+
if self.should_simulate_error():
|
|
112
|
+
error_response = self._generate_error_response(is_openai_format)
|
|
113
|
+
return 429, {"content-type": "application/json"}, error_response
|
|
114
|
+
|
|
115
|
+
# Generate mock response based on type
|
|
116
|
+
if message_type == "tool_use":
|
|
117
|
+
mock_response = self.mock_generator.generate_tool_use_response(model=model)
|
|
118
|
+
elif message_type == "long":
|
|
119
|
+
mock_response = self.mock_generator.generate_long_response(model=model)
|
|
120
|
+
elif message_type == "medium":
|
|
121
|
+
mock_response = self.mock_generator.generate_medium_response(model=model)
|
|
122
|
+
else:
|
|
123
|
+
mock_response = self.mock_generator.generate_short_response(model=model)
|
|
124
|
+
|
|
125
|
+
# Convert to OpenAI format if needed
|
|
126
|
+
if is_openai_format and message_type != "tool_use":
|
|
127
|
+
# Use dict-based conversion
|
|
128
|
+
mock_response = await self.openai_adapter.convert_response(mock_response)
|
|
129
|
+
|
|
130
|
+
# Update context with metrics
|
|
131
|
+
if ctx:
|
|
132
|
+
ctx.metrics["mock_response_type"] = message_type
|
|
133
|
+
ctx.metrics["mock_latency_ms"] = int(latency * 1000)
|
|
134
|
+
|
|
135
|
+
headers = {
|
|
136
|
+
"content-type": "application/json",
|
|
137
|
+
"x-request-id": ctx.request_id if ctx else "mock-request",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return 200, headers, json.dumps(mock_response).encode()
|
|
141
|
+
|
|
142
|
+
async def generate_streaming_response(
|
|
143
|
+
self,
|
|
144
|
+
model: str | None,
|
|
145
|
+
is_openai_format: bool,
|
|
146
|
+
ctx: RequestContext,
|
|
147
|
+
message_type: str = "short",
|
|
148
|
+
) -> StreamingResponse:
|
|
149
|
+
"""Generate SSE streaming mock response.
|
|
150
|
+
|
|
151
|
+
- Simulates realistic token generation rate
|
|
152
|
+
- Properly formatted SSE events
|
|
153
|
+
- Includes [DONE] marker
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
async def stream_generator() -> AsyncGenerator[bytes, None]:
|
|
157
|
+
# Generate base response
|
|
158
|
+
if message_type == "tool_use":
|
|
159
|
+
base_response = self.mock_generator.generate_tool_use_response(
|
|
160
|
+
model=model
|
|
161
|
+
)
|
|
162
|
+
elif message_type == "long":
|
|
163
|
+
base_response = self.mock_generator.generate_long_response(model=model)
|
|
164
|
+
else:
|
|
165
|
+
base_response = self.mock_generator.generate_short_response(model=model)
|
|
166
|
+
|
|
167
|
+
content = base_response.get("content", [{"text": "Mock response"}])
|
|
168
|
+
if isinstance(content, list) and content:
|
|
169
|
+
text_content = content[0].get("text", "Mock response")
|
|
170
|
+
else:
|
|
171
|
+
text_content = "Mock response"
|
|
172
|
+
|
|
173
|
+
# Split content into chunks
|
|
174
|
+
words = text_content.split()
|
|
175
|
+
chunk_size = 3 # Words per chunk
|
|
176
|
+
|
|
177
|
+
# Send initial event
|
|
178
|
+
if is_openai_format:
|
|
179
|
+
initial_event = {
|
|
180
|
+
"id": f"chatcmpl-{ctx.request_id if ctx else 'mock'}",
|
|
181
|
+
"object": "chat.completion.chunk",
|
|
182
|
+
"created": 1234567890,
|
|
183
|
+
"model": model or "gpt-4",
|
|
184
|
+
"choices": [
|
|
185
|
+
{
|
|
186
|
+
"index": 0,
|
|
187
|
+
"delta": {"role": "assistant"},
|
|
188
|
+
"finish_reason": None,
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
}
|
|
192
|
+
yield f"data: {json.dumps(initial_event)}\n\n".encode()
|
|
193
|
+
else:
|
|
194
|
+
initial_event = {
|
|
195
|
+
"type": "message_start",
|
|
196
|
+
"message": {
|
|
197
|
+
"id": f"msg_{ctx.request_id if ctx else 'mock'}",
|
|
198
|
+
"type": "message",
|
|
199
|
+
"role": "assistant",
|
|
200
|
+
"model": model or "claude-3-opus-20240229",
|
|
201
|
+
"content": [],
|
|
202
|
+
"usage": {"input_tokens": 10, "output_tokens": 0},
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
yield f"data: {json.dumps(initial_event)}\n\n".encode()
|
|
206
|
+
|
|
207
|
+
# Stream content chunks
|
|
208
|
+
for i in range(0, len(words), chunk_size):
|
|
209
|
+
chunk_words = words[i : i + chunk_size]
|
|
210
|
+
chunk_text = " ".join(chunk_words)
|
|
211
|
+
if i + chunk_size < len(words):
|
|
212
|
+
chunk_text += " "
|
|
213
|
+
|
|
214
|
+
await asyncio.sleep(0.05) # Simulate token generation delay
|
|
215
|
+
|
|
216
|
+
if is_openai_format:
|
|
217
|
+
chunk_event = {
|
|
218
|
+
"id": f"chatcmpl-{ctx.request_id if ctx else 'mock'}",
|
|
219
|
+
"object": "chat.completion.chunk",
|
|
220
|
+
"created": 1234567890,
|
|
221
|
+
"model": model or "gpt-4",
|
|
222
|
+
"choices": [
|
|
223
|
+
{
|
|
224
|
+
"index": 0,
|
|
225
|
+
"delta": {"content": chunk_text},
|
|
226
|
+
"finish_reason": None,
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
else:
|
|
231
|
+
chunk_event = {
|
|
232
|
+
"type": "content_block_delta",
|
|
233
|
+
"index": 0,
|
|
234
|
+
"delta": {"type": "text_delta", "text": chunk_text},
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
yield f"data: {json.dumps(chunk_event)}\n\n".encode()
|
|
238
|
+
|
|
239
|
+
# Send final event
|
|
240
|
+
if is_openai_format:
|
|
241
|
+
final_event = {
|
|
242
|
+
"id": f"chatcmpl-{ctx.request_id if ctx else 'mock'}",
|
|
243
|
+
"object": "chat.completion.chunk",
|
|
244
|
+
"created": 1234567890,
|
|
245
|
+
"model": model or "gpt-4",
|
|
246
|
+
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
|
|
247
|
+
}
|
|
248
|
+
yield f"data: {json.dumps(final_event)}\n\n".encode()
|
|
249
|
+
else:
|
|
250
|
+
final_event = {
|
|
251
|
+
"type": "message_stop",
|
|
252
|
+
"message": {
|
|
253
|
+
"usage": {
|
|
254
|
+
"input_tokens": 10,
|
|
255
|
+
"output_tokens": len(text_content.split()),
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
yield f"data: {json.dumps(final_event)}\n\n".encode()
|
|
260
|
+
|
|
261
|
+
# Send [DONE] marker
|
|
262
|
+
yield b"data: [DONE]\n\n"
|
|
263
|
+
|
|
264
|
+
return StreamingResponse(
|
|
265
|
+
stream_generator(),
|
|
266
|
+
media_type="text/event-stream",
|
|
267
|
+
headers={
|
|
268
|
+
"Cache-Control": "no-cache",
|
|
269
|
+
"X-Request-ID": ctx.request_id if ctx else "mock-request",
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _generate_error_response(self, is_openai_format: bool) -> bytes:
|
|
274
|
+
"""Generate a mock error response."""
|
|
275
|
+
if is_openai_format:
|
|
276
|
+
error: dict[str, Any] = {
|
|
277
|
+
"error": {
|
|
278
|
+
"message": "Rate limit exceeded (mock error)",
|
|
279
|
+
"type": "rate_limit_error",
|
|
280
|
+
"code": "rate_limit_exceeded",
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else:
|
|
284
|
+
error = {
|
|
285
|
+
"type": "error",
|
|
286
|
+
"error": {
|
|
287
|
+
"type": "rate_limit_error",
|
|
288
|
+
"message": "Rate limit exceeded (mock error)",
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
return json.dumps(error).encode()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Request tracing services for monitoring and debugging."""
|
|
2
|
+
|
|
3
|
+
from ccproxy.services.tracing.interfaces import RequestTracer, StreamingTracer
|
|
4
|
+
from ccproxy.services.tracing.null_tracer import NullRequestTracer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = ["RequestTracer", "StreamingTracer", "NullRequestTracer"]
|