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,140 @@
|
|
|
1
|
+
"""Base adapter interface for API format conversion."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from ccproxy.core.interfaces import StreamingConfigurable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
RequestType = TypeVar("RequestType", bound=BaseModel)
|
|
13
|
+
ResponseType = TypeVar("ResponseType", bound=BaseModel)
|
|
14
|
+
StreamEventType = TypeVar("StreamEventType", bound=BaseModel)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class APIAdapter(ABC, Generic[RequestType, ResponseType, StreamEventType]):
|
|
18
|
+
"""Abstract base class for API format adapters.
|
|
19
|
+
|
|
20
|
+
Provides strongly-typed interface for converting between different API formats
|
|
21
|
+
with full type safety and validation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def adapt_request(self, request: RequestType) -> BaseModel:
|
|
26
|
+
"""Convert a request using strongly-typed Pydantic models.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
request: The typed request model to convert
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The converted typed request model
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If the request format is invalid or unsupported
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def adapt_response(self, response: ResponseType) -> BaseModel:
|
|
41
|
+
"""Convert a response using strongly-typed Pydantic models.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
response: The typed response model to convert
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The converted typed response model
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If the response format is invalid or unsupported
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def adapt_stream(
|
|
56
|
+
self, stream: AsyncIterator[StreamEventType]
|
|
57
|
+
) -> AsyncGenerator[BaseModel, None]:
|
|
58
|
+
"""Convert a streaming response using strongly-typed Pydantic models.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
stream: The typed streaming response data to convert
|
|
62
|
+
|
|
63
|
+
Yields:
|
|
64
|
+
The converted typed streaming response chunks
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If the stream format is invalid or unsupported
|
|
68
|
+
"""
|
|
69
|
+
# This should be implemented as an async generator
|
|
70
|
+
# Subclasses must override this method
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
async def adapt_error(self, error: BaseModel) -> BaseModel:
|
|
75
|
+
"""Convert an error response using strongly-typed Pydantic models.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
error: The typed error response model to convert
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The converted typed error response model
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If the error format is invalid or unsupported
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BaseAPIAdapter(
|
|
90
|
+
APIAdapter[RequestType, ResponseType, StreamEventType],
|
|
91
|
+
StreamingConfigurable,
|
|
92
|
+
):
|
|
93
|
+
"""Base implementation with common functionality.
|
|
94
|
+
|
|
95
|
+
Provides strongly-typed interface for API format conversion with
|
|
96
|
+
better type safety and validation.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, name: str):
|
|
100
|
+
self.name = name
|
|
101
|
+
# Optional streaming flags that subclasses may use
|
|
102
|
+
self._openai_thinking_xml: bool | None = None
|
|
103
|
+
|
|
104
|
+
def __str__(self) -> str:
|
|
105
|
+
return f"{self.__class__.__name__}({self.name})"
|
|
106
|
+
|
|
107
|
+
def __repr__(self) -> str:
|
|
108
|
+
return self.__str__()
|
|
109
|
+
|
|
110
|
+
# StreamingConfigurable
|
|
111
|
+
def configure_streaming(self, *, openai_thinking_xml: bool | None = None) -> None:
|
|
112
|
+
self._openai_thinking_xml = openai_thinking_xml
|
|
113
|
+
|
|
114
|
+
# Strongly-typed interface - subclasses implement these
|
|
115
|
+
@abstractmethod
|
|
116
|
+
async def adapt_request(self, request: RequestType) -> BaseModel:
|
|
117
|
+
"""Convert a request using strongly-typed Pydantic models."""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
async def adapt_response(self, response: ResponseType) -> BaseModel:
|
|
122
|
+
"""Convert a response using strongly-typed Pydantic models."""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def adapt_stream(
|
|
127
|
+
self, stream: AsyncIterator[StreamEventType]
|
|
128
|
+
) -> AsyncGenerator[BaseModel, None]:
|
|
129
|
+
"""Convert a streaming response using strongly-typed Pydantic models."""
|
|
130
|
+
# This should be implemented as an async generator
|
|
131
|
+
# Subclasses must override this method
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
async def adapt_error(self, error: BaseModel) -> BaseModel:
|
|
136
|
+
"""Convert an error response using strongly-typed Pydantic models."""
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
__all__ = ["APIAdapter", "BaseAPIAdapter"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Shared base model for all LLM API models."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LlmBaseModel(BaseModel):
|
|
9
|
+
"""Base model for all LLM API models with proper JSON serialization.
|
|
10
|
+
|
|
11
|
+
Excludes None values and empty collections to match API conventions.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(
|
|
15
|
+
extra="allow", # Allow extra fields
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
19
|
+
"""Override to exclude empty collections as well as None values."""
|
|
20
|
+
# Extract exclude_none from kwargs, defaulting to True for our convention
|
|
21
|
+
exclude_none = kwargs.pop("exclude_none", True)
|
|
22
|
+
# First get the data with None values excluded
|
|
23
|
+
data = super().model_dump(exclude_none=exclude_none, **kwargs)
|
|
24
|
+
|
|
25
|
+
# Filter out empty collections (lists, dicts, sets)
|
|
26
|
+
filtered_data = {}
|
|
27
|
+
for key, value in data.items():
|
|
28
|
+
if isinstance(value, list | dict | set) and len(value) == 0:
|
|
29
|
+
# Skip empty collections
|
|
30
|
+
continue
|
|
31
|
+
filtered_data[key] = value
|
|
32
|
+
|
|
33
|
+
return filtered_data
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Shared helpers used by formatter adapters."""
|
|
2
|
+
|
|
3
|
+
from .identifiers import ensure_identifier, normalize_suffix
|
|
4
|
+
from .streams import (
|
|
5
|
+
IndexedToolCallTracker,
|
|
6
|
+
ObfuscationTokenFactory,
|
|
7
|
+
ReasoningBuffer,
|
|
8
|
+
ReasoningPartState,
|
|
9
|
+
ToolCallState,
|
|
10
|
+
ToolCallTracker,
|
|
11
|
+
build_anthropic_tool_use_block,
|
|
12
|
+
emit_anthropic_tool_use_events,
|
|
13
|
+
)
|
|
14
|
+
from .thinking import (
|
|
15
|
+
THINKING_CLOSE_PATTERN,
|
|
16
|
+
THINKING_OPEN_PATTERN,
|
|
17
|
+
THINKING_PATTERN,
|
|
18
|
+
ThinkingSegment,
|
|
19
|
+
merge_thinking_segments,
|
|
20
|
+
)
|
|
21
|
+
from .usage import (
|
|
22
|
+
convert_anthropic_usage_to_openai_completion_usage,
|
|
23
|
+
convert_anthropic_usage_to_openai_responses_usage,
|
|
24
|
+
convert_openai_completion_usage_to_responses_usage,
|
|
25
|
+
convert_openai_responses_usage_to_anthropic_usage,
|
|
26
|
+
convert_openai_responses_usage_to_completion_usage,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ensure_identifier",
|
|
32
|
+
"normalize_suffix",
|
|
33
|
+
"THINKING_PATTERN",
|
|
34
|
+
"THINKING_OPEN_PATTERN",
|
|
35
|
+
"THINKING_CLOSE_PATTERN",
|
|
36
|
+
"ThinkingSegment",
|
|
37
|
+
"merge_thinking_segments",
|
|
38
|
+
"ReasoningBuffer",
|
|
39
|
+
"ReasoningPartState",
|
|
40
|
+
"ToolCallState",
|
|
41
|
+
"ToolCallTracker",
|
|
42
|
+
"IndexedToolCallTracker",
|
|
43
|
+
"ObfuscationTokenFactory",
|
|
44
|
+
"build_anthropic_tool_use_block",
|
|
45
|
+
"emit_anthropic_tool_use_events",
|
|
46
|
+
"convert_anthropic_usage_to_openai_completion_usage",
|
|
47
|
+
"convert_anthropic_usage_to_openai_responses_usage",
|
|
48
|
+
"convert_openai_completion_usage_to_responses_usage",
|
|
49
|
+
"convert_openai_responses_usage_to_anthropic_usage",
|
|
50
|
+
"convert_openai_responses_usage_to_completion_usage",
|
|
51
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Identifier helpers shared across formatter adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize_suffix(identifier: str) -> str:
|
|
9
|
+
"""Return the suffix part of an identifier split on the first underscore."""
|
|
10
|
+
|
|
11
|
+
if "_" in identifier:
|
|
12
|
+
return identifier.split("_", 1)[1]
|
|
13
|
+
return identifier
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_identifier(prefix: str, existing: str | None = None) -> tuple[str, str]:
|
|
17
|
+
"""Return a stable identifier and suffix for the given prefix.
|
|
18
|
+
|
|
19
|
+
If an existing identifier already matches the prefix we reuse its suffix.
|
|
20
|
+
Existing identifiers that begin with ``resp_`` are also understood so both
|
|
21
|
+
``resp`` and alternate prefixes can build consistent derived identifiers.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if isinstance(existing, str) and existing.startswith(f"{prefix}_"):
|
|
25
|
+
return existing, normalize_suffix(existing)
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
isinstance(existing, str)
|
|
29
|
+
and existing
|
|
30
|
+
and prefix == "resp"
|
|
31
|
+
and existing.startswith("resp_")
|
|
32
|
+
):
|
|
33
|
+
return existing, normalize_suffix(existing)
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
isinstance(existing, str)
|
|
37
|
+
and existing
|
|
38
|
+
and existing.startswith("resp_")
|
|
39
|
+
and prefix != "resp"
|
|
40
|
+
):
|
|
41
|
+
suffix = normalize_suffix(existing)
|
|
42
|
+
return f"{prefix}_{suffix}", suffix
|
|
43
|
+
|
|
44
|
+
suffix = uuid.uuid4().hex
|
|
45
|
+
return f"{prefix}_{suffix}", suffix
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["ensure_identifier", "normalize_suffix"]
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Shared streaming helpers for formatter adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ccproxy.llms.formatters.utils import build_obfuscation_token
|
|
10
|
+
from ccproxy.llms.models import anthropic as anthropic_models
|
|
11
|
+
|
|
12
|
+
from .thinking import ThinkingSegment
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class ReasoningPartState:
|
|
17
|
+
"""Mutable reasoning buffer for a specific summary segment."""
|
|
18
|
+
|
|
19
|
+
buffer: list[str] = field(default_factory=list)
|
|
20
|
+
signature: str | None = None
|
|
21
|
+
open: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReasoningBuffer:
|
|
25
|
+
"""Utility to manage reasoning text buffers keyed by item/summary ids."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._states: dict[str, dict[Any, ReasoningPartState]] = {}
|
|
29
|
+
|
|
30
|
+
def ensure_part(self, item_id: str, summary_index: Any) -> ReasoningPartState:
|
|
31
|
+
item_states = self._states.setdefault(item_id, {})
|
|
32
|
+
part_state = item_states.get(summary_index)
|
|
33
|
+
if part_state is None:
|
|
34
|
+
part_state = ReasoningPartState()
|
|
35
|
+
item_states[summary_index] = part_state
|
|
36
|
+
return part_state
|
|
37
|
+
|
|
38
|
+
def set_signature(
|
|
39
|
+
self, item_id: str, summary_index: Any, signature: str | None
|
|
40
|
+
) -> None:
|
|
41
|
+
if not signature:
|
|
42
|
+
return
|
|
43
|
+
part_state = self.ensure_part(item_id, summary_index)
|
|
44
|
+
part_state.signature = signature
|
|
45
|
+
|
|
46
|
+
def reset_buffer(self, item_id: str, summary_index: Any) -> None:
|
|
47
|
+
part_state = self.ensure_part(item_id, summary_index)
|
|
48
|
+
part_state.buffer.clear()
|
|
49
|
+
|
|
50
|
+
def open_part(
|
|
51
|
+
self, item_id: str, summary_index: Any, signature: str | None = None
|
|
52
|
+
) -> ReasoningPartState:
|
|
53
|
+
part_state = self.ensure_part(item_id, summary_index)
|
|
54
|
+
if signature:
|
|
55
|
+
part_state.signature = signature
|
|
56
|
+
part_state.buffer.clear()
|
|
57
|
+
part_state.open = True
|
|
58
|
+
return part_state
|
|
59
|
+
|
|
60
|
+
def close_part(self, item_id: str, summary_index: Any) -> None:
|
|
61
|
+
part_state = self.ensure_part(item_id, summary_index)
|
|
62
|
+
part_state.open = False
|
|
63
|
+
|
|
64
|
+
def is_open(self, item_id: str, summary_index: Any) -> bool:
|
|
65
|
+
return self.ensure_part(item_id, summary_index).open
|
|
66
|
+
|
|
67
|
+
def append_text(self, item_id: str, summary_index: Any, text: str | None) -> None:
|
|
68
|
+
if not isinstance(text, str) or not text:
|
|
69
|
+
return
|
|
70
|
+
part_state = self.ensure_part(item_id, summary_index)
|
|
71
|
+
part_state.buffer.append(text)
|
|
72
|
+
|
|
73
|
+
def emit(
|
|
74
|
+
self, item_id: str, summary_index: Any, final_text: str | None = None
|
|
75
|
+
) -> list[str]:
|
|
76
|
+
part_state = self.ensure_part(item_id, summary_index)
|
|
77
|
+
text = (
|
|
78
|
+
final_text
|
|
79
|
+
if isinstance(final_text, str) and final_text
|
|
80
|
+
else "".join(part_state.buffer)
|
|
81
|
+
)
|
|
82
|
+
part_state.buffer.clear()
|
|
83
|
+
part_state.open = False
|
|
84
|
+
if not text:
|
|
85
|
+
return []
|
|
86
|
+
segment = ThinkingSegment(thinking=text, signature=part_state.signature)
|
|
87
|
+
xml = segment.to_xml()
|
|
88
|
+
closing = "</thinking>"
|
|
89
|
+
body = xml[: -len(closing)] if xml.endswith(closing) else xml
|
|
90
|
+
return [body, closing]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(slots=True)
|
|
94
|
+
class ToolCallState:
|
|
95
|
+
"""Mutable state for a single streaming tool call."""
|
|
96
|
+
|
|
97
|
+
id: str
|
|
98
|
+
index: int
|
|
99
|
+
call_id: str | None = None
|
|
100
|
+
item_id: str | None = None
|
|
101
|
+
name: str | None = None
|
|
102
|
+
arguments: str = ""
|
|
103
|
+
arguments_parts: list[str] = field(default_factory=list)
|
|
104
|
+
output_index: int = -1
|
|
105
|
+
emitted: bool = False
|
|
106
|
+
initial_emitted: bool = False
|
|
107
|
+
name_emitted: bool = False
|
|
108
|
+
arguments_emitted: bool = False
|
|
109
|
+
arguments_done_emitted: bool = False
|
|
110
|
+
item_done_emitted: bool = False
|
|
111
|
+
added_emitted: bool = False
|
|
112
|
+
completed: bool = False
|
|
113
|
+
final_arguments: str | None = None
|
|
114
|
+
|
|
115
|
+
def append_arguments(self, segment: str) -> None:
|
|
116
|
+
if segment:
|
|
117
|
+
self.arguments += segment
|
|
118
|
+
|
|
119
|
+
def add_arguments_part(self, segment: str) -> None:
|
|
120
|
+
if segment:
|
|
121
|
+
self.arguments_parts.append(segment)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ToolCallTracker:
|
|
125
|
+
"""Registry tracking streaming tool calls by item identifier."""
|
|
126
|
+
|
|
127
|
+
def __init__(self) -> None:
|
|
128
|
+
self._states: dict[str, ToolCallState] = {}
|
|
129
|
+
self._order: list[str] = []
|
|
130
|
+
|
|
131
|
+
def ensure(self, item_id: str) -> ToolCallState:
|
|
132
|
+
state = self._states.get(item_id)
|
|
133
|
+
if state is None:
|
|
134
|
+
state = ToolCallState(
|
|
135
|
+
id=item_id,
|
|
136
|
+
index=len(self._order),
|
|
137
|
+
)
|
|
138
|
+
state.output_index = len(self._order)
|
|
139
|
+
self._states[item_id] = state
|
|
140
|
+
self._order.append(item_id)
|
|
141
|
+
return state
|
|
142
|
+
|
|
143
|
+
def values(self) -> list[ToolCallState]:
|
|
144
|
+
return [self._states[item_id] for item_id in self._order]
|
|
145
|
+
|
|
146
|
+
def any_completed(self) -> bool:
|
|
147
|
+
return any(state.completed for state in self._states.values())
|
|
148
|
+
|
|
149
|
+
def __len__(self) -> int: # noqa: D401
|
|
150
|
+
return len(self._states)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class IndexedToolCallTracker:
|
|
154
|
+
"""Registry tracking streaming tool calls keyed by integer index."""
|
|
155
|
+
|
|
156
|
+
def __init__(self) -> None:
|
|
157
|
+
self._states: dict[int, ToolCallState] = {}
|
|
158
|
+
|
|
159
|
+
def ensure(self, index: int) -> ToolCallState:
|
|
160
|
+
state = self._states.get(index)
|
|
161
|
+
if state is None:
|
|
162
|
+
state = ToolCallState(id=f"call_{index}", index=index)
|
|
163
|
+
self._states[index] = state
|
|
164
|
+
return state
|
|
165
|
+
|
|
166
|
+
def items(self) -> list[tuple[int, ToolCallState]]:
|
|
167
|
+
return [(idx, self._states[idx]) for idx in sorted(self._states)]
|
|
168
|
+
|
|
169
|
+
def values(self) -> list[ToolCallState]:
|
|
170
|
+
return [state for _, state in self.items()]
|
|
171
|
+
|
|
172
|
+
def __contains__(self, index: int) -> bool: # noqa: D401
|
|
173
|
+
return index in self._states
|
|
174
|
+
|
|
175
|
+
def __len__(self) -> int: # noqa: D401
|
|
176
|
+
return len(self._states)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ObfuscationTokenFactory:
|
|
180
|
+
"""Utility for building deterministic obfuscation tokens."""
|
|
181
|
+
|
|
182
|
+
def __init__(self, fallback_identifier: Callable[[], str]) -> None:
|
|
183
|
+
self._fallback_identifier = fallback_identifier
|
|
184
|
+
|
|
185
|
+
def make(
|
|
186
|
+
self,
|
|
187
|
+
kind: str,
|
|
188
|
+
*,
|
|
189
|
+
sequence: int,
|
|
190
|
+
item_id: str | None = None,
|
|
191
|
+
payload: str | None = None,
|
|
192
|
+
) -> str:
|
|
193
|
+
base_identifier = item_id or self._fallback_identifier()
|
|
194
|
+
return build_obfuscation_token(
|
|
195
|
+
seed=f"{kind}:{base_identifier}",
|
|
196
|
+
sequence=sequence,
|
|
197
|
+
payload=payload or "",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build_anthropic_tool_use_block(
|
|
202
|
+
state: ToolCallState,
|
|
203
|
+
*,
|
|
204
|
+
default_id: str | None = None,
|
|
205
|
+
parser: Callable[[str], dict[str, Any]] | None = None,
|
|
206
|
+
) -> anthropic_models.ToolUseBlock:
|
|
207
|
+
"""Create an Anthropic ToolUseBlock from a tracked tool-call state."""
|
|
208
|
+
|
|
209
|
+
tool_id = state.item_id or state.call_id or default_id or f"call_{state.index}"
|
|
210
|
+
arguments_text = (
|
|
211
|
+
state.final_arguments or state.arguments or "".join(state.arguments_parts)
|
|
212
|
+
)
|
|
213
|
+
parse_input = parser or (lambda text: {"arguments": text} if text else {})
|
|
214
|
+
input_payload = parse_input(arguments_text)
|
|
215
|
+
|
|
216
|
+
return anthropic_models.ToolUseBlock(
|
|
217
|
+
type="tool_use",
|
|
218
|
+
id=str(tool_id),
|
|
219
|
+
name=str(state.name or "tool"),
|
|
220
|
+
input=input_payload,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def emit_anthropic_tool_use_events(
|
|
225
|
+
index: int,
|
|
226
|
+
state: ToolCallState,
|
|
227
|
+
*,
|
|
228
|
+
parser: Callable[[str], dict[str, Any]] | None = None,
|
|
229
|
+
) -> list[anthropic_models.MessageStreamEvent]:
|
|
230
|
+
"""Build start/stop events for a tool-use block at the given index."""
|
|
231
|
+
|
|
232
|
+
block = build_anthropic_tool_use_block(
|
|
233
|
+
state,
|
|
234
|
+
default_id=f"call_{state.index}",
|
|
235
|
+
parser=parser,
|
|
236
|
+
)
|
|
237
|
+
return [
|
|
238
|
+
anthropic_models.ContentBlockStartEvent(
|
|
239
|
+
type="content_block_start", index=index, content_block=block
|
|
240
|
+
),
|
|
241
|
+
anthropic_models.ContentBlockStopEvent(type="content_block_stop", index=index),
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
__all__ = [
|
|
246
|
+
"ReasoningBuffer",
|
|
247
|
+
"ReasoningPartState",
|
|
248
|
+
"ToolCallState",
|
|
249
|
+
"ToolCallTracker",
|
|
250
|
+
"IndexedToolCallTracker",
|
|
251
|
+
"ObfuscationTokenFactory",
|
|
252
|
+
"build_anthropic_tool_use_block",
|
|
253
|
+
"emit_anthropic_tool_use_events",
|
|
254
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Shared helpers for reasoning/thinking segment handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from ccproxy.llms.models import anthropic as anthropic_models
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
THINKING_PATTERN = re.compile(
|
|
13
|
+
r"<thinking(?:\s+signature=\"([^\"]*)\")?>(.*?)</thinking>",
|
|
14
|
+
re.DOTALL,
|
|
15
|
+
)
|
|
16
|
+
THINKING_OPEN_PATTERN = re.compile(
|
|
17
|
+
r"<thinking(?:\s+signature=\"([^\"]*)\")?\s*>",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
THINKING_CLOSE_PATTERN = re.compile(r"</thinking>", re.IGNORECASE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class ThinkingSegment:
|
|
25
|
+
"""Lightweight reasoning segment mirroring Anthropic's ThinkingBlock."""
|
|
26
|
+
|
|
27
|
+
thinking: str
|
|
28
|
+
signature: str | None = None
|
|
29
|
+
|
|
30
|
+
def to_block(self) -> anthropic_models.ThinkingBlock:
|
|
31
|
+
return anthropic_models.ThinkingBlock(
|
|
32
|
+
type="thinking",
|
|
33
|
+
thinking=self.thinking,
|
|
34
|
+
signature=self.signature or "",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def to_xml(self) -> str:
|
|
38
|
+
signature = (self.signature or "").strip()
|
|
39
|
+
signature_attr = f' signature="{signature}"' if signature else ""
|
|
40
|
+
return f"<thinking{signature_attr}>{self.thinking}</thinking>"
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_xml(cls, signature: str | None, text: str) -> ThinkingSegment:
|
|
44
|
+
return cls(thinking=text, signature=signature or None)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def merge_thinking_segments(
|
|
48
|
+
segments: Iterable[ThinkingSegment],
|
|
49
|
+
) -> list[ThinkingSegment]:
|
|
50
|
+
"""Collapse adjacent segments that share the same signature."""
|
|
51
|
+
|
|
52
|
+
merged: list[ThinkingSegment] = []
|
|
53
|
+
for segment in segments:
|
|
54
|
+
text = segment.thinking if isinstance(segment.thinking, str) else None
|
|
55
|
+
if not text:
|
|
56
|
+
continue
|
|
57
|
+
signature = segment.signature or None
|
|
58
|
+
if merged and merged[-1].signature == signature:
|
|
59
|
+
merged[-1] = ThinkingSegment(
|
|
60
|
+
thinking=f"{merged[-1].thinking}{text}",
|
|
61
|
+
signature=signature,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
merged.append(ThinkingSegment(thinking=text, signature=signature))
|
|
65
|
+
return merged
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"THINKING_PATTERN",
|
|
70
|
+
"THINKING_OPEN_PATTERN",
|
|
71
|
+
"THINKING_CLOSE_PATTERN",
|
|
72
|
+
"ThinkingSegment",
|
|
73
|
+
"merge_thinking_segments",
|
|
74
|
+
]
|