ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Base adapter for provider plugins."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
from starlette.responses import Response, StreamingResponse
|
|
8
|
+
|
|
9
|
+
from ccproxy.streaming import DeferredStreaming
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseAdapter(ABC):
|
|
13
|
+
"""Base adapter for provider-specific request handling."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: Any, **kwargs: Any) -> None:
|
|
16
|
+
"""Initialize the base adapter.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config: Plugin configuration
|
|
20
|
+
**kwargs: Additional keyword arguments for subclasses
|
|
21
|
+
"""
|
|
22
|
+
self.config = config
|
|
23
|
+
self.tool_accumulator_class = kwargs.pop("tool_accumulator_class", None)
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def handle_request(
|
|
27
|
+
self, request: Request
|
|
28
|
+
) -> Response | StreamingResponse | DeferredStreaming:
|
|
29
|
+
"""Handle a provider-specific request.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
request: FastAPI request object with endpoint and method in request.state.context
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Response, StreamingResponse, or DeferredStreaming object
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def handle_streaming(
|
|
41
|
+
self, request: Request, endpoint: str, **kwargs: Any
|
|
42
|
+
) -> StreamingResponse | DeferredStreaming:
|
|
43
|
+
"""Handle a streaming request.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
request: FastAPI request object
|
|
47
|
+
endpoint: Target endpoint path
|
|
48
|
+
**kwargs: Additional provider-specific arguments
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
StreamingResponse or DeferredStreaming object
|
|
52
|
+
"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
async def validate_request(
|
|
56
|
+
self, request: Request, endpoint: str
|
|
57
|
+
) -> dict[str, Any] | None:
|
|
58
|
+
"""Validate request before processing.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
request: FastAPI request object
|
|
62
|
+
endpoint: Target endpoint path
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Validation result or None if valid
|
|
66
|
+
"""
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
async def transform_request(self, request_data: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
+
"""Transform request data if needed.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
request_data: Original request data
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Transformed request data
|
|
77
|
+
"""
|
|
78
|
+
return request_data
|
|
79
|
+
|
|
80
|
+
async def transform_response(self, response_data: dict[str, Any]) -> dict[str, Any]:
|
|
81
|
+
"""Transform response data if needed.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
response_data: Original response data
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Transformed response data
|
|
88
|
+
"""
|
|
89
|
+
return response_data
|
|
90
|
+
|
|
91
|
+
def _ensure_tool_accumulator(self, request_context: Any) -> None:
|
|
92
|
+
"""Attach tool accumulator metadata to the request context if available."""
|
|
93
|
+
|
|
94
|
+
if not self.tool_accumulator_class or not request_context:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
if getattr(request_context, "_tool_accumulator_class", None) is None:
|
|
98
|
+
request_context._tool_accumulator_class = self.tool_accumulator_class
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _record_tool_definitions(request_context: Any, payload: Any) -> None:
|
|
102
|
+
"""Persist tool definitions on the request context for downstream consumers."""
|
|
103
|
+
|
|
104
|
+
if not request_context or not isinstance(payload, dict):
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
metadata = getattr(request_context, "metadata", None)
|
|
108
|
+
if not isinstance(metadata, dict):
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
tools = payload.get("tools")
|
|
112
|
+
if tools and "_tool_definitions" not in metadata:
|
|
113
|
+
metadata["_tool_definitions"] = tools
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
async def cleanup(self) -> None:
|
|
117
|
+
"""Cleanup adapter resources.
|
|
118
|
+
|
|
119
|
+
This method should be overridden by concrete adapters to clean up
|
|
120
|
+
any resources like HTTP clients, sessions, or background tasks.
|
|
121
|
+
Called during application shutdown.
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from .format_adapter import DictFormatAdapter, FormatAdapterProtocol
|
|
8
|
+
from .format_registry import FormatRegistry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ComposedAdapter(DictFormatAdapter):
|
|
12
|
+
"""A DictFormatAdapter composed from multiple pairwise adapters."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _pairs_from_chain(
|
|
18
|
+
chain: list[str], stage: Literal["request", "response", "error", "stream"]
|
|
19
|
+
) -> list[tuple[str, str]]:
|
|
20
|
+
if len(chain) < 2:
|
|
21
|
+
return []
|
|
22
|
+
# For responses and streaming, convert from provider format (tail) back to client format (head)
|
|
23
|
+
if stage in ("response", "error", "stream"):
|
|
24
|
+
pairs = [(chain[i + 1], chain[i]) for i in range(len(chain) - 1)]
|
|
25
|
+
pairs.reverse()
|
|
26
|
+
return pairs
|
|
27
|
+
# Requests go forward (client -> provider)
|
|
28
|
+
return [(chain[i], chain[i + 1]) for i in range(len(chain) - 1)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compose_from_chain(
|
|
32
|
+
*,
|
|
33
|
+
registry: FormatRegistry,
|
|
34
|
+
chain: list[str],
|
|
35
|
+
name: str | None = None,
|
|
36
|
+
) -> FormatAdapterProtocol:
|
|
37
|
+
"""Compose a FormatAdapter from a format_chain using the registry.
|
|
38
|
+
|
|
39
|
+
The composed adapter sequentially applies the per‑pair adapters for request,
|
|
40
|
+
response, error, and stream stages.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
async def _compose_stage(
|
|
44
|
+
data: dict[str, Any], stage: Literal["request", "response", "error"]
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
current = data
|
|
47
|
+
for src, dst in _pairs_from_chain(chain, stage):
|
|
48
|
+
adapter = registry.get(src, dst)
|
|
49
|
+
if stage == "request":
|
|
50
|
+
current = await adapter.convert_request(current)
|
|
51
|
+
elif stage == "response":
|
|
52
|
+
current = await adapter.convert_response(current)
|
|
53
|
+
else:
|
|
54
|
+
# Default error passthrough if adapter lacks explicit error handling
|
|
55
|
+
with contextlib.suppress(NotImplementedError):
|
|
56
|
+
current = await adapter.convert_error(current)
|
|
57
|
+
return current
|
|
58
|
+
|
|
59
|
+
async def _request(data: dict[str, Any]) -> dict[str, Any]:
|
|
60
|
+
return await _compose_stage(data, "request")
|
|
61
|
+
|
|
62
|
+
async def _response(data: dict[str, Any]) -> dict[str, Any]:
|
|
63
|
+
return await _compose_stage(data, "response")
|
|
64
|
+
|
|
65
|
+
async def _error(data: dict[str, Any]) -> dict[str, Any]:
|
|
66
|
+
return await _compose_stage(data, "error")
|
|
67
|
+
|
|
68
|
+
async def _stream(
|
|
69
|
+
stream: AsyncIterator[dict[str, Any]],
|
|
70
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
71
|
+
# Pipe the stream through each pairwise adapter's convert_stream
|
|
72
|
+
current_stream = stream
|
|
73
|
+
for src, dst in _pairs_from_chain(chain, "stream"):
|
|
74
|
+
adapter = registry.get(src, dst)
|
|
75
|
+
current_stream = adapter.convert_stream(current_stream)
|
|
76
|
+
async for item in current_stream:
|
|
77
|
+
yield item
|
|
78
|
+
|
|
79
|
+
return ComposedAdapter(
|
|
80
|
+
request=_request,
|
|
81
|
+
response=_response,
|
|
82
|
+
error=_error,
|
|
83
|
+
stream=_stream,
|
|
84
|
+
name=name or f"ComposedAdapter({' -> '.join(chain)})",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["compose_from_chain", "ComposedAdapter"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from .format_registry import FormatRegistry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_chains(
|
|
9
|
+
*, registry: FormatRegistry, chains: Iterable[list[str]]
|
|
10
|
+
) -> list[str]:
|
|
11
|
+
"""Validate that all adjacent pairs in chains exist in the registry.
|
|
12
|
+
|
|
13
|
+
Returns a list of human‑readable error strings for missing pairs.
|
|
14
|
+
"""
|
|
15
|
+
errors: list[str] = []
|
|
16
|
+
pairs_needed: set[tuple[str, str]] = set()
|
|
17
|
+
for chain in chains:
|
|
18
|
+
if len(chain) >= 2:
|
|
19
|
+
for i in range(len(chain) - 1):
|
|
20
|
+
pairs_needed.add((chain[i], chain[i + 1]))
|
|
21
|
+
for src, dst in sorted(pairs_needed):
|
|
22
|
+
if registry.get_if_exists(src, dst) is None:
|
|
23
|
+
errors.append(f"Missing format adapter: {src} -> {dst}")
|
|
24
|
+
return errors
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_stream_pairs(
|
|
28
|
+
*, registry: FormatRegistry, chains: Iterable[list[str]]
|
|
29
|
+
) -> list[str]:
|
|
30
|
+
"""Validate reverse-direction pairs for streaming (provider→client)."""
|
|
31
|
+
missing: list[str] = []
|
|
32
|
+
for chain in chains:
|
|
33
|
+
if len(chain) < 2:
|
|
34
|
+
continue
|
|
35
|
+
reverse_pairs = list(
|
|
36
|
+
reversed([(chain[i + 1], chain[i]) for i in range(len(chain) - 1)])
|
|
37
|
+
)
|
|
38
|
+
for src, dst in reverse_pairs:
|
|
39
|
+
if registry.get_if_exists(src, dst) is None:
|
|
40
|
+
missing.append(f"Missing streaming adapter: {src} -> {dst}")
|
|
41
|
+
return missing
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = ["validate_chains", "validate_stream_pairs"]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""ChatCompletion accumulator for OpenAI streaming format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .delta_utils import accumulate_delta
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChatCompletionAccumulator:
|
|
12
|
+
"""Accumulator for OpenAI ChatCompletion streaming format.
|
|
13
|
+
|
|
14
|
+
Handles partial tool calls and other streaming data by accumulating
|
|
15
|
+
chunks until complete objects are ready for validation.
|
|
16
|
+
|
|
17
|
+
Follows the OpenAI SDK ChatCompletionStreamManager pattern.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._accumulated: dict[str, Any] = {}
|
|
22
|
+
self._done_tool_calls: set[int] = set()
|
|
23
|
+
self._current_tool_call_index: int | None = None
|
|
24
|
+
|
|
25
|
+
def accumulate_chunk(self, chunk: dict[str, Any]) -> dict[str, Any] | None:
|
|
26
|
+
"""Accumulate a streaming chunk and return complete object if ready.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
chunk: The incoming stream chunk data
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
None if accumulation is ongoing, or the complete object when ready
|
|
33
|
+
for validation
|
|
34
|
+
"""
|
|
35
|
+
# For chunks without tool calls, return immediately UNLESS we have accumulated state
|
|
36
|
+
# (in which case this might be a finish_reason chunk)
|
|
37
|
+
if not self._has_tool_calls(chunk) and not self._accumulated:
|
|
38
|
+
return chunk
|
|
39
|
+
|
|
40
|
+
# For the first chunk, copy the base structure
|
|
41
|
+
if not self._accumulated:
|
|
42
|
+
self._accumulated = copy.deepcopy(chunk)
|
|
43
|
+
else:
|
|
44
|
+
# For subsequent chunks, preserve base fields and only accumulate deltas
|
|
45
|
+
base_fields = {"id", "object", "created", "model"}
|
|
46
|
+
chunk_copy = copy.deepcopy(chunk)
|
|
47
|
+
|
|
48
|
+
# Remove base fields from chunk_copy to avoid concatenation
|
|
49
|
+
for field in base_fields:
|
|
50
|
+
if field in chunk_copy:
|
|
51
|
+
del chunk_copy[field]
|
|
52
|
+
|
|
53
|
+
# Use accumulate_delta for the remaining fields (choices, etc.)
|
|
54
|
+
self._accumulated = accumulate_delta(self._accumulated, chunk_copy)
|
|
55
|
+
|
|
56
|
+
# Track tool call progress if present
|
|
57
|
+
if self._has_tool_calls(chunk):
|
|
58
|
+
self._track_tool_call_progress(chunk)
|
|
59
|
+
|
|
60
|
+
# Don't validate if we have incomplete tool calls
|
|
61
|
+
if self._has_incomplete_tool_calls():
|
|
62
|
+
return None # Continue accumulating
|
|
63
|
+
|
|
64
|
+
# Return a copy for validation if chunk seems complete
|
|
65
|
+
if self._should_emit_chunk(chunk):
|
|
66
|
+
return copy.deepcopy(self._accumulated)
|
|
67
|
+
|
|
68
|
+
# Continue accumulating
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def reset(self) -> None:
|
|
72
|
+
"""Reset accumulator state for next message."""
|
|
73
|
+
self._accumulated.clear()
|
|
74
|
+
self._done_tool_calls.clear()
|
|
75
|
+
self._current_tool_call_index = None
|
|
76
|
+
|
|
77
|
+
def _has_tool_calls(self, chunk: dict[str, Any]) -> bool:
|
|
78
|
+
"""Check if chunk contains tool call data."""
|
|
79
|
+
if not isinstance(chunk, dict) or "choices" not in chunk:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
for choice in chunk.get("choices", []):
|
|
83
|
+
if not isinstance(choice, dict):
|
|
84
|
+
continue
|
|
85
|
+
delta = choice.get("delta", {})
|
|
86
|
+
if isinstance(delta, dict) and "tool_calls" in delta:
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def _track_tool_call_progress(self, chunk: dict[str, Any]) -> None:
|
|
92
|
+
"""Track progress of tool calls in this chunk."""
|
|
93
|
+
for choice in chunk.get("choices", []):
|
|
94
|
+
if not isinstance(choice, dict):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
delta = choice.get("delta", {})
|
|
98
|
+
if not isinstance(delta, dict):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
tool_calls = delta.get("tool_calls", [])
|
|
102
|
+
if not isinstance(tool_calls, list):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
for tool_call in tool_calls:
|
|
106
|
+
if not isinstance(tool_call, dict):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# Track current tool call index
|
|
110
|
+
if "index" in tool_call:
|
|
111
|
+
self._current_tool_call_index = tool_call["index"]
|
|
112
|
+
|
|
113
|
+
# Mark tool call as done if it has complete structure
|
|
114
|
+
if self._is_tool_call_complete(tool_call):
|
|
115
|
+
index = tool_call.get("index", self._current_tool_call_index)
|
|
116
|
+
if index is not None:
|
|
117
|
+
self._done_tool_calls.add(index)
|
|
118
|
+
|
|
119
|
+
def _is_tool_call_complete(self, tool_call: dict[str, Any]) -> bool:
|
|
120
|
+
"""Check if a tool call has all required fields."""
|
|
121
|
+
if not tool_call.get("id"):
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
function = tool_call.get("function", {})
|
|
125
|
+
if not isinstance(function, dict):
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
if not function.get("name"):
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Arguments can be empty string, but should be present
|
|
132
|
+
return "arguments" in function
|
|
133
|
+
|
|
134
|
+
def _has_incomplete_tool_calls(self) -> bool:
|
|
135
|
+
"""Check if accumulated state has incomplete tool calls."""
|
|
136
|
+
if not self._accumulated.get("choices"):
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
for choice in self._accumulated["choices"]:
|
|
140
|
+
if not isinstance(choice, dict):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
delta = choice.get("delta", {})
|
|
144
|
+
if not isinstance(delta, dict):
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
tool_calls = delta.get("tool_calls", [])
|
|
148
|
+
if not isinstance(tool_calls, list):
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
for tool_call in tool_calls:
|
|
152
|
+
if not isinstance(tool_call, dict):
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
# Check if this tool call is incomplete
|
|
156
|
+
if not self._is_tool_call_complete(tool_call):
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
def _should_emit_chunk(self, chunk: dict[str, Any]) -> bool:
|
|
162
|
+
"""Determine if we should emit the accumulated chunk for validation.
|
|
163
|
+
|
|
164
|
+
We emit when:
|
|
165
|
+
1. No tool calls are present (regular content)
|
|
166
|
+
2. All tool calls in the accumulated state are complete AND we see a finish_reason
|
|
167
|
+
"""
|
|
168
|
+
# If no tool calls in accumulated state, emit immediately
|
|
169
|
+
if not self._has_any_tool_calls_in_accumulated():
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
# For tool calls, only emit when both conditions are met:
|
|
173
|
+
# 1. All tool calls are complete
|
|
174
|
+
# 2. We see a finish_reason (indicates end of tool call sequence)
|
|
175
|
+
has_finish_reason = False
|
|
176
|
+
for choice in chunk.get("choices", []):
|
|
177
|
+
if isinstance(choice, dict) and choice.get("finish_reason"):
|
|
178
|
+
has_finish_reason = True
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
return bool(has_finish_reason and not self._has_incomplete_tool_calls())
|
|
182
|
+
|
|
183
|
+
def _has_any_tool_calls_in_accumulated(self) -> bool:
|
|
184
|
+
"""Check if accumulated state has any tool calls."""
|
|
185
|
+
if not self._accumulated.get("choices"):
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
for choice in self._accumulated["choices"]:
|
|
189
|
+
if not isinstance(choice, dict):
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
delta = choice.get("delta", {})
|
|
193
|
+
if not isinstance(delta, dict):
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
tool_calls = delta.get("tool_calls", [])
|
|
197
|
+
if isinstance(tool_calls, list) and tool_calls:
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
return False
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Delta accumulation utilities following OpenAI SDK patterns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def accumulate_delta(
|
|
9
|
+
accumulated: dict[str, Any], delta: dict[str, Any]
|
|
10
|
+
) -> dict[str, Any]:
|
|
11
|
+
"""Recursively merge delta into accumulated following OpenAI's rules.
|
|
12
|
+
|
|
13
|
+
This function implements the same accumulation logic as OpenAI's SDK:
|
|
14
|
+
- Concatenate strings
|
|
15
|
+
- Add numbers (int/float)
|
|
16
|
+
- Recursively merge dictionaries
|
|
17
|
+
- Extend primitive lists
|
|
18
|
+
- Merge object lists by 'index' key
|
|
19
|
+
- Preserve 'index' and 'type' keys without modification
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
accumulated: The accumulated state to merge into
|
|
23
|
+
delta: The delta to merge
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The merged result (may modify accumulated in-place)
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
TypeError: For unsupported data types
|
|
30
|
+
ValueError: For invalid list structures
|
|
31
|
+
"""
|
|
32
|
+
# Handle None/empty cases
|
|
33
|
+
if not delta:
|
|
34
|
+
return accumulated
|
|
35
|
+
if not accumulated:
|
|
36
|
+
return dict(delta)
|
|
37
|
+
|
|
38
|
+
# Work on a copy to avoid mutating input
|
|
39
|
+
result = dict(accumulated)
|
|
40
|
+
|
|
41
|
+
for key, delta_value in delta.items():
|
|
42
|
+
if key not in result:
|
|
43
|
+
# New key, just set it
|
|
44
|
+
result[key] = delta_value
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
current_value = result[key]
|
|
48
|
+
|
|
49
|
+
# Handle different data type combinations
|
|
50
|
+
if isinstance(current_value, str) and isinstance(delta_value, str):
|
|
51
|
+
# Concatenate strings
|
|
52
|
+
result[key] = current_value + delta_value
|
|
53
|
+
|
|
54
|
+
elif isinstance(current_value, int | float) and isinstance(
|
|
55
|
+
delta_value, int | float
|
|
56
|
+
):
|
|
57
|
+
# Add numbers
|
|
58
|
+
result[key] = current_value + delta_value
|
|
59
|
+
|
|
60
|
+
elif isinstance(current_value, dict) and isinstance(delta_value, dict):
|
|
61
|
+
# Recursively merge dictionaries
|
|
62
|
+
result[key] = accumulate_delta(current_value, delta_value)
|
|
63
|
+
|
|
64
|
+
elif isinstance(current_value, list) and isinstance(delta_value, list):
|
|
65
|
+
# Handle list merging
|
|
66
|
+
result[key] = _accumulate_list(current_value, delta_value)
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
# For any other case, delta value overwrites
|
|
70
|
+
result[key] = delta_value
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _accumulate_list(current: list[Any], delta: list[Any]) -> list[Any]:
|
|
76
|
+
"""Accumulate list values following OpenAI's patterns.
|
|
77
|
+
|
|
78
|
+
- For primitive lists: extend
|
|
79
|
+
- For object lists: merge by 'index' key
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
current: Current list value
|
|
83
|
+
delta: Delta list value
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Merged list
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If object list entries are missing required 'index' key
|
|
90
|
+
"""
|
|
91
|
+
if not delta:
|
|
92
|
+
return current
|
|
93
|
+
if not current:
|
|
94
|
+
return list(delta)
|
|
95
|
+
|
|
96
|
+
# Check if this is an object list (contains dicts with 'index')
|
|
97
|
+
has_indexed_objects = any(
|
|
98
|
+
isinstance(item, dict) and "index" in item for item in (current + delta)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if not has_indexed_objects:
|
|
102
|
+
# Primitive list - just extend
|
|
103
|
+
return current + delta
|
|
104
|
+
|
|
105
|
+
# Object list - merge by index
|
|
106
|
+
result = list(current)
|
|
107
|
+
|
|
108
|
+
for delta_item in delta:
|
|
109
|
+
if not isinstance(delta_item, dict):
|
|
110
|
+
# Mixed list types - append non-dict items
|
|
111
|
+
result.append(delta_item)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if "index" not in delta_item:
|
|
115
|
+
raise ValueError("Dictionary in list delta must have 'index' key")
|
|
116
|
+
|
|
117
|
+
delta_index = delta_item["index"]
|
|
118
|
+
|
|
119
|
+
# Find existing item with same index
|
|
120
|
+
existing_item = None
|
|
121
|
+
existing_pos = None
|
|
122
|
+
for i, item in enumerate(result):
|
|
123
|
+
if isinstance(item, dict) and item.get("index") == delta_index:
|
|
124
|
+
existing_item = item
|
|
125
|
+
existing_pos = i
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
if existing_item is not None and existing_pos is not None:
|
|
129
|
+
# Merge with existing item, preserving special keys
|
|
130
|
+
merged = accumulate_delta(existing_item, delta_item)
|
|
131
|
+
|
|
132
|
+
# Preserve 'index' and 'type' from original if not in delta
|
|
133
|
+
for special_key in ["index", "type"]:
|
|
134
|
+
if special_key not in delta_item and special_key in existing_item:
|
|
135
|
+
merged[special_key] = existing_item[special_key]
|
|
136
|
+
|
|
137
|
+
result[existing_pos] = merged
|
|
138
|
+
else:
|
|
139
|
+
# New item - append to list
|
|
140
|
+
result.append(delta_item)
|
|
141
|
+
|
|
142
|
+
return result
|