ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1546 @@
|
|
|
1
|
+
"""Anthropic→OpenAI streaming conversion entry points."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
8
|
+
from typing import Any, Literal, cast
|
|
9
|
+
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
import ccproxy.core.logging
|
|
13
|
+
from ccproxy.llms.formatters.common import (
|
|
14
|
+
IndexedToolCallTracker,
|
|
15
|
+
ObfuscationTokenFactory,
|
|
16
|
+
ToolCallState,
|
|
17
|
+
ensure_identifier,
|
|
18
|
+
)
|
|
19
|
+
from ccproxy.llms.formatters.constants import ANTHROPIC_TO_OPENAI_FINISH_REASON
|
|
20
|
+
from ccproxy.llms.formatters.context import (
|
|
21
|
+
get_last_instructions,
|
|
22
|
+
get_last_request,
|
|
23
|
+
register_request,
|
|
24
|
+
)
|
|
25
|
+
from ccproxy.llms.formatters.utils import anthropic_usage_snapshot
|
|
26
|
+
from ccproxy.llms.models import anthropic as anthropic_models
|
|
27
|
+
from ccproxy.llms.models import openai as openai_models
|
|
28
|
+
from ccproxy.llms.streaming.accumulators import ClaudeAccumulator
|
|
29
|
+
|
|
30
|
+
from .requests import _build_responses_payload_from_anthropic_request
|
|
31
|
+
from .responses import (
|
|
32
|
+
convert__anthropic_usage_to_openai_responses__usage,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
logger = ccproxy.core.logging.get_logger(__name__)
|
|
37
|
+
|
|
38
|
+
FinishReason = Literal["stop", "length", "tool_calls"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _normalize_anthropic_stream_event(
|
|
42
|
+
event: Any,
|
|
43
|
+
) -> tuple[str | None, dict[str, Any]]:
|
|
44
|
+
"""Return a (type, payload) tuple for mixed dict/model stream events."""
|
|
45
|
+
|
|
46
|
+
if isinstance(event, dict):
|
|
47
|
+
event_type = event.get("type") or event.get("event")
|
|
48
|
+
return (cast(str | None, event_type), event)
|
|
49
|
+
|
|
50
|
+
event_type = getattr(event, "type", None)
|
|
51
|
+
if event_type is None:
|
|
52
|
+
return None, {}
|
|
53
|
+
|
|
54
|
+
if hasattr(event, "model_dump"):
|
|
55
|
+
payload = cast(dict[str, Any], event.model_dump(mode="json"))
|
|
56
|
+
elif hasattr(event, "dict"):
|
|
57
|
+
payload = cast(dict[str, Any], event.dict())
|
|
58
|
+
else:
|
|
59
|
+
payload = {}
|
|
60
|
+
|
|
61
|
+
return cast(str | None, event_type), payload
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _anthropic_delta_to_text(
|
|
65
|
+
accumulator: ClaudeAccumulator,
|
|
66
|
+
block_index: int,
|
|
67
|
+
delta: dict[str, Any] | None,
|
|
68
|
+
) -> str | None:
|
|
69
|
+
if not isinstance(delta, dict):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
block_info = accumulator.get_block_info(block_index)
|
|
73
|
+
block_meta = block_info[1] if block_info else {}
|
|
74
|
+
block_type = block_meta.get("type")
|
|
75
|
+
|
|
76
|
+
if block_type == "thinking":
|
|
77
|
+
thinking_text = delta.get("thinking")
|
|
78
|
+
if not isinstance(thinking_text, str) or not thinking_text:
|
|
79
|
+
return None
|
|
80
|
+
signature = block_meta.get("signature")
|
|
81
|
+
if isinstance(signature, str) and signature:
|
|
82
|
+
return f'<thinking signature="{signature}">{thinking_text}</thinking>'
|
|
83
|
+
return f"<thinking>{thinking_text}</thinking>"
|
|
84
|
+
|
|
85
|
+
text_val = delta.get("text")
|
|
86
|
+
if isinstance(text_val, str) and text_val:
|
|
87
|
+
return text_val
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _build_openai_tool_call(
|
|
93
|
+
accumulator: ClaudeAccumulator,
|
|
94
|
+
block_index: int,
|
|
95
|
+
) -> openai_models.ToolCall | None:
|
|
96
|
+
for tool_call in accumulator.get_complete_tool_calls():
|
|
97
|
+
if tool_call.get("index") != block_index:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
function_payload = (
|
|
101
|
+
tool_call.get("function", {}) if isinstance(tool_call, dict) else {}
|
|
102
|
+
)
|
|
103
|
+
name = function_payload.get("name") or tool_call.get("name") or "function"
|
|
104
|
+
arguments = function_payload.get("arguments")
|
|
105
|
+
if not isinstance(arguments, str) or not arguments:
|
|
106
|
+
try:
|
|
107
|
+
arguments = json.dumps(tool_call.get("input", {}), ensure_ascii=False)
|
|
108
|
+
except Exception:
|
|
109
|
+
arguments = json.dumps(tool_call.get("input", {}))
|
|
110
|
+
|
|
111
|
+
tool_id = tool_call.get("id") or f"call_{block_index}"
|
|
112
|
+
|
|
113
|
+
return openai_models.ToolCall(
|
|
114
|
+
id=str(tool_id),
|
|
115
|
+
function=openai_models.FunctionCall(
|
|
116
|
+
name=str(name),
|
|
117
|
+
arguments=str(arguments),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AnthropicToOpenAIResponsesStreamAdapter:
|
|
125
|
+
"""Stateful adapter for Anthropic → OpenAI Responses streaming."""
|
|
126
|
+
|
|
127
|
+
async def run(
|
|
128
|
+
self,
|
|
129
|
+
stream: AsyncIterator[anthropic_models.MessageStreamEvent],
|
|
130
|
+
) -> AsyncGenerator[openai_models.StreamEventType, None]:
|
|
131
|
+
async for event in self._convert_responses_stream(stream):
|
|
132
|
+
yield event
|
|
133
|
+
|
|
134
|
+
async def _convert_responses_stream(
|
|
135
|
+
self,
|
|
136
|
+
stream: AsyncIterator[anthropic_models.MessageStreamEvent],
|
|
137
|
+
) -> AsyncGenerator[openai_models.StreamEventType, None]:
|
|
138
|
+
"""Convert Anthropic MessageStreamEvents into OpenAI Responses stream events."""
|
|
139
|
+
|
|
140
|
+
accumulator = ClaudeAccumulator()
|
|
141
|
+
sequence_counter = -1
|
|
142
|
+
model_id = ""
|
|
143
|
+
response_id = ""
|
|
144
|
+
id_suffix: str | None = None
|
|
145
|
+
message_item_id = ""
|
|
146
|
+
message_output_index: int | None = None
|
|
147
|
+
next_output_index = 0
|
|
148
|
+
content_index = 0
|
|
149
|
+
message_item_added = False
|
|
150
|
+
message_content_part_added = False
|
|
151
|
+
text_buffer: list[str] = []
|
|
152
|
+
message_last_logprobs: Any | None = None
|
|
153
|
+
message_text_done_emitted = False
|
|
154
|
+
message_part_done_emitted = False
|
|
155
|
+
message_item_done_emitted = False
|
|
156
|
+
message_completed_entry: tuple[int, openai_models.MessageOutput] | None = None
|
|
157
|
+
latest_usage_model: openai_models.ResponseUsage | None = None
|
|
158
|
+
final_stop_reason: str | None = None
|
|
159
|
+
stream_completed = False
|
|
160
|
+
|
|
161
|
+
reasoning_item_id = ""
|
|
162
|
+
reasoning_output_index: int | None = None
|
|
163
|
+
reasoning_item_added = False
|
|
164
|
+
reasoning_output_done = False
|
|
165
|
+
reasoning_summary_indices: dict[str, int] = {}
|
|
166
|
+
reasoning_summary_added: set[int] = set()
|
|
167
|
+
reasoning_summary_text_fragments: dict[int, list[str]] = {}
|
|
168
|
+
reasoning_summary_text_done: set[int] = set()
|
|
169
|
+
reasoning_summary_part_done: set[int] = set()
|
|
170
|
+
reasoning_completed_entry: tuple[int, openai_models.ReasoningOutput] | None = (
|
|
171
|
+
None
|
|
172
|
+
)
|
|
173
|
+
next_reasoning_summary_index = 0
|
|
174
|
+
reasoning_summary_signatures: dict[int, str | None] = {}
|
|
175
|
+
created_at_value: int | None = None
|
|
176
|
+
|
|
177
|
+
instructions_text = get_last_instructions()
|
|
178
|
+
if not instructions_text:
|
|
179
|
+
try:
|
|
180
|
+
from ccproxy.core.request_context import RequestContext
|
|
181
|
+
|
|
182
|
+
ctx = RequestContext.get_current()
|
|
183
|
+
if ctx is not None:
|
|
184
|
+
instr = ctx.metadata.get("instructions")
|
|
185
|
+
if isinstance(instr, str) and instr.strip():
|
|
186
|
+
instructions_text = instr.strip()
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
instructions_value = instructions_text or None
|
|
191
|
+
|
|
192
|
+
envelope_base_kwargs: dict[str, Any] = {
|
|
193
|
+
"id": "",
|
|
194
|
+
"object": "response",
|
|
195
|
+
"created_at": 0,
|
|
196
|
+
"instructions": instructions_value,
|
|
197
|
+
}
|
|
198
|
+
reasoning_summary_payload: list[dict[str, Any]] | None = None
|
|
199
|
+
|
|
200
|
+
last_request = get_last_request()
|
|
201
|
+
anthropic_request: anthropic_models.CreateMessageRequest | None = None
|
|
202
|
+
if isinstance(last_request, anthropic_models.CreateMessageRequest):
|
|
203
|
+
anthropic_request = last_request
|
|
204
|
+
elif isinstance(last_request, dict):
|
|
205
|
+
try:
|
|
206
|
+
anthropic_request = (
|
|
207
|
+
anthropic_models.CreateMessageRequest.model_validate(last_request)
|
|
208
|
+
)
|
|
209
|
+
except ValidationError:
|
|
210
|
+
anthropic_request = None
|
|
211
|
+
|
|
212
|
+
base_parallel_tool_calls = True
|
|
213
|
+
text_payload: dict[str, Any] | None = None
|
|
214
|
+
|
|
215
|
+
if anthropic_request is not None:
|
|
216
|
+
payload_data, _ = _build_responses_payload_from_anthropic_request(
|
|
217
|
+
anthropic_request
|
|
218
|
+
)
|
|
219
|
+
base_parallel_tool_calls = bool(
|
|
220
|
+
payload_data.get("parallel_tool_calls", True)
|
|
221
|
+
)
|
|
222
|
+
background_value = payload_data.get("background", None)
|
|
223
|
+
envelope_base_kwargs["background"] = (
|
|
224
|
+
bool(background_value) if background_value is not None else None
|
|
225
|
+
)
|
|
226
|
+
for key in (
|
|
227
|
+
"max_output_tokens",
|
|
228
|
+
"tool_choice",
|
|
229
|
+
"tools",
|
|
230
|
+
"service_tier",
|
|
231
|
+
"temperature",
|
|
232
|
+
"prompt_cache_key",
|
|
233
|
+
"top_p",
|
|
234
|
+
"metadata",
|
|
235
|
+
):
|
|
236
|
+
if key in payload_data:
|
|
237
|
+
envelope_base_kwargs[key] = payload_data[key]
|
|
238
|
+
text_payload = payload_data.get("text")
|
|
239
|
+
else:
|
|
240
|
+
envelope_base_kwargs["background"] = None
|
|
241
|
+
|
|
242
|
+
if text_payload is None:
|
|
243
|
+
text_payload = {"format": {"type": "text"}}
|
|
244
|
+
else:
|
|
245
|
+
text_payload = dict(text_payload)
|
|
246
|
+
text_payload.setdefault("verbosity", "low")
|
|
247
|
+
envelope_base_kwargs["text"] = text_payload
|
|
248
|
+
|
|
249
|
+
if "store" not in envelope_base_kwargs:
|
|
250
|
+
envelope_base_kwargs["store"] = True
|
|
251
|
+
|
|
252
|
+
if "temperature" not in envelope_base_kwargs:
|
|
253
|
+
temp_value = None
|
|
254
|
+
if anthropic_request is not None:
|
|
255
|
+
temp_value = anthropic_request.temperature
|
|
256
|
+
envelope_base_kwargs["temperature"] = (
|
|
257
|
+
temp_value if temp_value is not None else 1.0
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if "service_tier" not in envelope_base_kwargs:
|
|
261
|
+
service_value = None
|
|
262
|
+
if anthropic_request is not None:
|
|
263
|
+
service_value = anthropic_request.service_tier
|
|
264
|
+
envelope_base_kwargs["service_tier"] = service_value or "auto"
|
|
265
|
+
|
|
266
|
+
if "top_p" not in envelope_base_kwargs:
|
|
267
|
+
top_p_value = None
|
|
268
|
+
if anthropic_request is not None:
|
|
269
|
+
top_p_value = anthropic_request.top_p
|
|
270
|
+
envelope_base_kwargs["top_p"] = (
|
|
271
|
+
top_p_value if top_p_value is not None else 1.0
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if "metadata" not in envelope_base_kwargs:
|
|
275
|
+
envelope_base_kwargs["metadata"] = {}
|
|
276
|
+
|
|
277
|
+
reasoning_effort = None
|
|
278
|
+
if anthropic_request is not None:
|
|
279
|
+
thinking_cfg = getattr(anthropic_request, "thinking", None)
|
|
280
|
+
if getattr(thinking_cfg, "type", None) == "enabled":
|
|
281
|
+
reasoning_effort = "medium"
|
|
282
|
+
envelope_base_kwargs["reasoning"] = openai_models.Reasoning(
|
|
283
|
+
effort=reasoning_effort,
|
|
284
|
+
summary=None,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if "tool_choice" not in envelope_base_kwargs:
|
|
288
|
+
envelope_base_kwargs["tool_choice"] = "auto"
|
|
289
|
+
if "tools" not in envelope_base_kwargs:
|
|
290
|
+
envelope_base_kwargs["tools"] = []
|
|
291
|
+
|
|
292
|
+
parallel_setting_initial = bool(base_parallel_tool_calls)
|
|
293
|
+
envelope_base_kwargs["parallel_tool_calls"] = parallel_setting_initial
|
|
294
|
+
|
|
295
|
+
tool_states = IndexedToolCallTracker()
|
|
296
|
+
obfuscation_factory = ObfuscationTokenFactory(
|
|
297
|
+
lambda: id_suffix or response_id or "stream"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def ensure_message_output_item() -> list[openai_models.StreamEventType]:
|
|
301
|
+
nonlocal message_item_added, message_output_index, next_output_index
|
|
302
|
+
events: list[openai_models.StreamEventType] = []
|
|
303
|
+
if message_output_index is None:
|
|
304
|
+
message_output_index = next_output_index
|
|
305
|
+
next_output_index += 1
|
|
306
|
+
if not message_item_added:
|
|
307
|
+
message_item_added = True
|
|
308
|
+
nonlocal sequence_counter
|
|
309
|
+
sequence_counter += 1
|
|
310
|
+
events.append(
|
|
311
|
+
openai_models.ResponseOutputItemAddedEvent(
|
|
312
|
+
type="response.output_item.added",
|
|
313
|
+
sequence_number=sequence_counter,
|
|
314
|
+
output_index=message_output_index,
|
|
315
|
+
item=openai_models.OutputItem(
|
|
316
|
+
id=message_item_id,
|
|
317
|
+
type="message",
|
|
318
|
+
role="assistant",
|
|
319
|
+
status="in_progress",
|
|
320
|
+
content=[],
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
return events
|
|
325
|
+
|
|
326
|
+
def ensure_message_content_part() -> list[openai_models.StreamEventType]:
|
|
327
|
+
events = ensure_message_output_item()
|
|
328
|
+
nonlocal message_content_part_added, sequence_counter
|
|
329
|
+
if not message_content_part_added and message_output_index is not None:
|
|
330
|
+
message_content_part_added = True
|
|
331
|
+
sequence_counter += 1
|
|
332
|
+
events.append(
|
|
333
|
+
openai_models.ResponseContentPartAddedEvent(
|
|
334
|
+
type="response.content_part.added",
|
|
335
|
+
sequence_number=sequence_counter,
|
|
336
|
+
item_id=message_item_id,
|
|
337
|
+
output_index=message_output_index,
|
|
338
|
+
content_index=content_index,
|
|
339
|
+
part=openai_models.ContentPart(
|
|
340
|
+
type="output_text",
|
|
341
|
+
text="",
|
|
342
|
+
annotations=[],
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
return events
|
|
347
|
+
|
|
348
|
+
def emit_message_text_delta(
|
|
349
|
+
text_delta: str,
|
|
350
|
+
*,
|
|
351
|
+
logprobs: Any | None = None,
|
|
352
|
+
obfuscation: str | None = None,
|
|
353
|
+
) -> list[openai_models.StreamEventType]:
|
|
354
|
+
if not isinstance(text_delta, str) or not text_delta:
|
|
355
|
+
return []
|
|
356
|
+
|
|
357
|
+
nonlocal sequence_counter, message_last_logprobs, message_item_done_emitted
|
|
358
|
+
if message_item_done_emitted:
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
events = ensure_message_content_part()
|
|
362
|
+
sequence_counter += 1
|
|
363
|
+
event_sequence = sequence_counter
|
|
364
|
+
logprobs_value: Any = [] if logprobs is None else logprobs
|
|
365
|
+
events.append(
|
|
366
|
+
openai_models.ResponseOutputTextDeltaEvent(
|
|
367
|
+
type="response.output_text.delta",
|
|
368
|
+
sequence_number=event_sequence,
|
|
369
|
+
item_id=message_item_id,
|
|
370
|
+
output_index=message_output_index or 0,
|
|
371
|
+
content_index=content_index,
|
|
372
|
+
delta=text_delta,
|
|
373
|
+
logprobs=logprobs_value,
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
text_buffer.append(text_delta)
|
|
377
|
+
message_last_logprobs = logprobs_value
|
|
378
|
+
return events
|
|
379
|
+
|
|
380
|
+
def _reasoning_key(signature: str | None) -> str:
|
|
381
|
+
if isinstance(signature, str) and signature.strip():
|
|
382
|
+
return signature.strip()
|
|
383
|
+
return "__default__"
|
|
384
|
+
|
|
385
|
+
def get_reasoning_summary_index(signature: str | None) -> int:
|
|
386
|
+
nonlocal next_reasoning_summary_index
|
|
387
|
+
key = _reasoning_key(signature)
|
|
388
|
+
existing = reasoning_summary_indices.get(key)
|
|
389
|
+
if existing is not None:
|
|
390
|
+
return existing
|
|
391
|
+
reasoning_summary_indices[key] = next_reasoning_summary_index
|
|
392
|
+
reasoning_summary_signatures[next_reasoning_summary_index] = signature
|
|
393
|
+
next_reasoning_summary_index += 1
|
|
394
|
+
return reasoning_summary_indices[key]
|
|
395
|
+
|
|
396
|
+
def ensure_reasoning_output_item() -> (
|
|
397
|
+
openai_models.ResponseOutputItemAddedEvent | None
|
|
398
|
+
):
|
|
399
|
+
nonlocal reasoning_item_added, reasoning_output_index
|
|
400
|
+
nonlocal sequence_counter, next_output_index
|
|
401
|
+
if reasoning_output_index is None:
|
|
402
|
+
reasoning_output_index = next_output_index
|
|
403
|
+
next_output_index += 1
|
|
404
|
+
if not reasoning_item_added:
|
|
405
|
+
reasoning_item_added = True
|
|
406
|
+
sequence_counter += 1
|
|
407
|
+
return openai_models.ResponseOutputItemAddedEvent(
|
|
408
|
+
type="response.output_item.added",
|
|
409
|
+
sequence_number=sequence_counter,
|
|
410
|
+
output_index=reasoning_output_index,
|
|
411
|
+
item=openai_models.OutputItem(
|
|
412
|
+
id=reasoning_item_id,
|
|
413
|
+
type="reasoning",
|
|
414
|
+
status="in_progress",
|
|
415
|
+
summary=[],
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
def ensure_reasoning_summary_part(
|
|
421
|
+
summary_index: int,
|
|
422
|
+
) -> openai_models.ReasoningSummaryPartAddedEvent | None:
|
|
423
|
+
nonlocal sequence_counter
|
|
424
|
+
if reasoning_output_index is None:
|
|
425
|
+
return None
|
|
426
|
+
if summary_index in reasoning_summary_added:
|
|
427
|
+
return None
|
|
428
|
+
reasoning_summary_added.add(summary_index)
|
|
429
|
+
sequence_counter += 1
|
|
430
|
+
return openai_models.ReasoningSummaryPartAddedEvent(
|
|
431
|
+
type="response.reasoning_summary_part.added",
|
|
432
|
+
sequence_number=sequence_counter,
|
|
433
|
+
item_id=reasoning_item_id,
|
|
434
|
+
output_index=reasoning_output_index,
|
|
435
|
+
summary_index=summary_index,
|
|
436
|
+
part=openai_models.ReasoningSummaryPart(
|
|
437
|
+
type="summary_text",
|
|
438
|
+
text="",
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def emit_reasoning_text_delta(
|
|
443
|
+
text_delta: str,
|
|
444
|
+
signature: str | None,
|
|
445
|
+
) -> list[openai_models.StreamEventType]:
|
|
446
|
+
if not isinstance(text_delta, str) or not text_delta:
|
|
447
|
+
return []
|
|
448
|
+
|
|
449
|
+
events: list[openai_models.StreamEventType] = []
|
|
450
|
+
output_event = ensure_reasoning_output_item()
|
|
451
|
+
if output_event is not None:
|
|
452
|
+
events.append(output_event)
|
|
453
|
+
|
|
454
|
+
summary_index = get_reasoning_summary_index(signature)
|
|
455
|
+
part_event = ensure_reasoning_summary_part(summary_index)
|
|
456
|
+
if part_event is not None:
|
|
457
|
+
events.append(part_event)
|
|
458
|
+
|
|
459
|
+
fragments = reasoning_summary_text_fragments.setdefault(summary_index, [])
|
|
460
|
+
fragments.append(text_delta)
|
|
461
|
+
if summary_index not in reasoning_summary_signatures:
|
|
462
|
+
reasoning_summary_signatures[summary_index] = signature
|
|
463
|
+
|
|
464
|
+
nonlocal sequence_counter
|
|
465
|
+
sequence_counter += 1
|
|
466
|
+
event_sequence = sequence_counter
|
|
467
|
+
events.append(
|
|
468
|
+
openai_models.ReasoningSummaryTextDeltaEvent(
|
|
469
|
+
type="response.reasoning_summary_text.delta",
|
|
470
|
+
sequence_number=event_sequence,
|
|
471
|
+
item_id=reasoning_item_id,
|
|
472
|
+
output_index=reasoning_output_index or 0,
|
|
473
|
+
summary_index=summary_index,
|
|
474
|
+
delta=text_delta,
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
return events
|
|
478
|
+
|
|
479
|
+
def finalize_reasoning() -> list[openai_models.StreamEventType]:
|
|
480
|
+
nonlocal reasoning_output_done, reasoning_completed_entry
|
|
481
|
+
nonlocal reasoning_summary_payload, sequence_counter
|
|
482
|
+
if not reasoning_item_added or reasoning_output_index is None:
|
|
483
|
+
return []
|
|
484
|
+
|
|
485
|
+
events: list[openai_models.StreamEventType] = []
|
|
486
|
+
summary_entries: list[dict[str, Any]] = []
|
|
487
|
+
|
|
488
|
+
for summary_index in sorted(reasoning_summary_text_fragments):
|
|
489
|
+
text_value = "".join(
|
|
490
|
+
reasoning_summary_text_fragments.get(summary_index, [])
|
|
491
|
+
)
|
|
492
|
+
if summary_index not in reasoning_summary_text_done:
|
|
493
|
+
sequence_counter += 1
|
|
494
|
+
events.append(
|
|
495
|
+
openai_models.ReasoningSummaryTextDoneEvent(
|
|
496
|
+
type="response.reasoning_summary_text.done",
|
|
497
|
+
sequence_number=sequence_counter,
|
|
498
|
+
item_id=reasoning_item_id,
|
|
499
|
+
output_index=reasoning_output_index,
|
|
500
|
+
summary_index=summary_index,
|
|
501
|
+
text=text_value,
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
reasoning_summary_text_done.add(summary_index)
|
|
505
|
+
if summary_index not in reasoning_summary_part_done:
|
|
506
|
+
sequence_counter += 1
|
|
507
|
+
events.append(
|
|
508
|
+
openai_models.ReasoningSummaryPartDoneEvent(
|
|
509
|
+
type="response.reasoning_summary_part.done",
|
|
510
|
+
sequence_number=sequence_counter,
|
|
511
|
+
item_id=reasoning_item_id,
|
|
512
|
+
output_index=reasoning_output_index,
|
|
513
|
+
summary_index=summary_index,
|
|
514
|
+
part=openai_models.ReasoningSummaryPart(
|
|
515
|
+
type="summary_text",
|
|
516
|
+
text=text_value,
|
|
517
|
+
),
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
reasoning_summary_part_done.add(summary_index)
|
|
521
|
+
summary_entry: dict[str, Any] = {
|
|
522
|
+
"type": "summary_text",
|
|
523
|
+
"text": text_value,
|
|
524
|
+
}
|
|
525
|
+
signature_value = reasoning_summary_signatures.get(summary_index)
|
|
526
|
+
if signature_value:
|
|
527
|
+
summary_entry["signature"] = signature_value
|
|
528
|
+
summary_entries.append(summary_entry)
|
|
529
|
+
|
|
530
|
+
reasoning_summary_payload = summary_entries
|
|
531
|
+
|
|
532
|
+
if not reasoning_output_done:
|
|
533
|
+
sequence_counter += 1
|
|
534
|
+
events.append(
|
|
535
|
+
openai_models.ResponseOutputItemDoneEvent(
|
|
536
|
+
type="response.output_item.done",
|
|
537
|
+
sequence_number=sequence_counter,
|
|
538
|
+
output_index=reasoning_output_index,
|
|
539
|
+
item=openai_models.OutputItem(
|
|
540
|
+
id=reasoning_item_id,
|
|
541
|
+
type="reasoning",
|
|
542
|
+
status="completed",
|
|
543
|
+
summary=summary_entries,
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
reasoning_output_done = True
|
|
548
|
+
reasoning_completed_entry = (
|
|
549
|
+
reasoning_output_index,
|
|
550
|
+
openai_models.ReasoningOutput(
|
|
551
|
+
type="reasoning",
|
|
552
|
+
id=reasoning_item_id,
|
|
553
|
+
status="completed",
|
|
554
|
+
summary=summary_entries,
|
|
555
|
+
),
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
return events
|
|
559
|
+
|
|
560
|
+
def ensure_tool_state(block_index: int) -> ToolCallState:
|
|
561
|
+
nonlocal next_output_index
|
|
562
|
+
state = tool_states.ensure(block_index)
|
|
563
|
+
if state.output_index < 0:
|
|
564
|
+
state.output_index = next_output_index
|
|
565
|
+
next_output_index += 1
|
|
566
|
+
return state
|
|
567
|
+
|
|
568
|
+
def emit_tool_item_added(
|
|
569
|
+
block_index: int, state: ToolCallState
|
|
570
|
+
) -> list[openai_models.StreamEventType]:
|
|
571
|
+
events: list[openai_models.StreamEventType] = []
|
|
572
|
+
if state.added_emitted:
|
|
573
|
+
return events
|
|
574
|
+
|
|
575
|
+
tool_entry = accumulator.get_tool_entry(block_index)
|
|
576
|
+
if tool_entry:
|
|
577
|
+
if not state.name:
|
|
578
|
+
state.name = tool_entry.get("function", {}).get(
|
|
579
|
+
"name"
|
|
580
|
+
) or tool_entry.get("name")
|
|
581
|
+
if not state.call_id:
|
|
582
|
+
state.call_id = tool_entry.get("id")
|
|
583
|
+
|
|
584
|
+
item_id = state.item_id or state.call_id or f"call_{state.index}"
|
|
585
|
+
state.item_id = item_id
|
|
586
|
+
|
|
587
|
+
name = state.name or "function"
|
|
588
|
+
|
|
589
|
+
nonlocal sequence_counter
|
|
590
|
+
sequence_counter += 1
|
|
591
|
+
events.append(
|
|
592
|
+
openai_models.ResponseOutputItemAddedEvent(
|
|
593
|
+
type="response.output_item.added",
|
|
594
|
+
sequence_number=sequence_counter,
|
|
595
|
+
output_index=state.output_index,
|
|
596
|
+
item=openai_models.OutputItem(
|
|
597
|
+
id=str(item_id),
|
|
598
|
+
type="function_call",
|
|
599
|
+
status="in_progress",
|
|
600
|
+
name=str(name),
|
|
601
|
+
arguments="",
|
|
602
|
+
call_id=state.call_id,
|
|
603
|
+
),
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
state.added_emitted = True
|
|
607
|
+
return events
|
|
608
|
+
|
|
609
|
+
def emit_tool_arguments_delta(
|
|
610
|
+
state: ToolCallState, delta_text: str
|
|
611
|
+
) -> openai_models.StreamEventType:
|
|
612
|
+
nonlocal sequence_counter
|
|
613
|
+
sequence_counter += 1
|
|
614
|
+
event_sequence = sequence_counter
|
|
615
|
+
state.add_arguments_part(delta_text)
|
|
616
|
+
item_identifier = str(state.item_id or f"call_{state.index}")
|
|
617
|
+
return openai_models.ResponseFunctionCallArgumentsDeltaEvent(
|
|
618
|
+
type="response.function_call_arguments.delta",
|
|
619
|
+
sequence_number=event_sequence,
|
|
620
|
+
item_id=item_identifier,
|
|
621
|
+
output_index=state.output_index,
|
|
622
|
+
delta=delta_text,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
def emit_tool_finalize(
|
|
626
|
+
block_index: int, state: ToolCallState
|
|
627
|
+
) -> list[openai_models.StreamEventType]:
|
|
628
|
+
events: list[openai_models.StreamEventType] = []
|
|
629
|
+
tool_entry = accumulator.get_tool_entry(block_index)
|
|
630
|
+
|
|
631
|
+
if tool_entry:
|
|
632
|
+
if not state.name:
|
|
633
|
+
state.name = tool_entry.get("function", {}).get(
|
|
634
|
+
"name"
|
|
635
|
+
) or tool_entry.get("name")
|
|
636
|
+
if not state.call_id:
|
|
637
|
+
state.call_id = tool_entry.get("id")
|
|
638
|
+
if not state.item_id:
|
|
639
|
+
state.item_id = tool_entry.get("id")
|
|
640
|
+
|
|
641
|
+
item_id = state.item_id or state.call_id or f"call_{state.index}"
|
|
642
|
+
state.item_id = item_id
|
|
643
|
+
name = state.name or "function"
|
|
644
|
+
|
|
645
|
+
args_str = "".join(state.arguments_parts)
|
|
646
|
+
if not args_str and tool_entry:
|
|
647
|
+
try:
|
|
648
|
+
args_str = json.dumps(
|
|
649
|
+
tool_entry.get("input", {}), ensure_ascii=False
|
|
650
|
+
)
|
|
651
|
+
except Exception:
|
|
652
|
+
args_str = json.dumps(tool_entry.get("input", {}))
|
|
653
|
+
|
|
654
|
+
nonlocal sequence_counter
|
|
655
|
+
if not state.added_emitted:
|
|
656
|
+
events.extend(emit_tool_item_added(block_index, state))
|
|
657
|
+
|
|
658
|
+
if not state.arguments_done_emitted:
|
|
659
|
+
sequence_counter += 1
|
|
660
|
+
events.append(
|
|
661
|
+
openai_models.ResponseFunctionCallArgumentsDoneEvent(
|
|
662
|
+
type="response.function_call_arguments.done",
|
|
663
|
+
sequence_number=sequence_counter,
|
|
664
|
+
item_id=str(item_id),
|
|
665
|
+
output_index=state.output_index,
|
|
666
|
+
arguments=args_str,
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
state.arguments_done_emitted = True
|
|
670
|
+
|
|
671
|
+
if not state.item_done_emitted:
|
|
672
|
+
sequence_counter += 1
|
|
673
|
+
events.append(
|
|
674
|
+
openai_models.ResponseOutputItemDoneEvent(
|
|
675
|
+
type="response.output_item.done",
|
|
676
|
+
sequence_number=sequence_counter,
|
|
677
|
+
output_index=state.output_index,
|
|
678
|
+
item=openai_models.OutputItem(
|
|
679
|
+
id=str(item_id),
|
|
680
|
+
type="function_call",
|
|
681
|
+
status="completed",
|
|
682
|
+
name=str(name),
|
|
683
|
+
arguments=args_str,
|
|
684
|
+
call_id=state.call_id,
|
|
685
|
+
),
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
state.item_done_emitted = True
|
|
689
|
+
state.final_arguments = args_str
|
|
690
|
+
|
|
691
|
+
return events
|
|
692
|
+
|
|
693
|
+
def finalize_message() -> list[openai_models.StreamEventType]:
|
|
694
|
+
nonlocal sequence_counter
|
|
695
|
+
nonlocal message_text_done_emitted, message_part_done_emitted
|
|
696
|
+
nonlocal message_item_done_emitted, message_completed_entry
|
|
697
|
+
nonlocal message_last_logprobs
|
|
698
|
+
nonlocal accumulator
|
|
699
|
+
|
|
700
|
+
if not message_item_added or message_output_index is None:
|
|
701
|
+
return []
|
|
702
|
+
|
|
703
|
+
events: list[openai_models.StreamEventType] = []
|
|
704
|
+
final_text = "".join(text_buffer)
|
|
705
|
+
logprobs_value: Any
|
|
706
|
+
if message_last_logprobs is None:
|
|
707
|
+
logprobs_value = []
|
|
708
|
+
else:
|
|
709
|
+
logprobs_value = message_last_logprobs
|
|
710
|
+
|
|
711
|
+
primary_text_part: openai_models.OutputTextContent | None = None
|
|
712
|
+
tool_and_aux_blocks: list[Any] = []
|
|
713
|
+
|
|
714
|
+
if accumulator.content_blocks:
|
|
715
|
+
sorted_blocks = sorted(
|
|
716
|
+
accumulator.content_blocks, key=lambda block: block.get("index", 0)
|
|
717
|
+
)
|
|
718
|
+
for block in sorted_blocks:
|
|
719
|
+
block_type = block.get("type")
|
|
720
|
+
if block_type == "text":
|
|
721
|
+
text_value = block.get("text", "")
|
|
722
|
+
part = openai_models.OutputTextContent(
|
|
723
|
+
type="output_text",
|
|
724
|
+
text=text_value,
|
|
725
|
+
annotations=[],
|
|
726
|
+
logprobs=logprobs_value if text_value else [],
|
|
727
|
+
)
|
|
728
|
+
if primary_text_part is None and text_value:
|
|
729
|
+
primary_text_part = part
|
|
730
|
+
tool_and_aux_blocks.append(part)
|
|
731
|
+
else:
|
|
732
|
+
block_payload = {k: v for k, v in block.items() if k != "index"}
|
|
733
|
+
if block_payload.get("type") == "tool_use":
|
|
734
|
+
tool_input = block_payload.get("input")
|
|
735
|
+
if tool_input is not None:
|
|
736
|
+
block_payload.setdefault("arguments", tool_input)
|
|
737
|
+
tool_and_aux_blocks.append(block_payload)
|
|
738
|
+
|
|
739
|
+
if primary_text_part is None and final_text:
|
|
740
|
+
primary_text_part = openai_models.OutputTextContent(
|
|
741
|
+
type="output_text",
|
|
742
|
+
text=final_text,
|
|
743
|
+
annotations=[],
|
|
744
|
+
logprobs=logprobs_value if final_text else [],
|
|
745
|
+
)
|
|
746
|
+
tool_and_aux_blocks.insert(0, primary_text_part)
|
|
747
|
+
|
|
748
|
+
if message_content_part_added and not message_text_done_emitted:
|
|
749
|
+
sequence_counter += 1
|
|
750
|
+
event_sequence = sequence_counter
|
|
751
|
+
events.append(
|
|
752
|
+
openai_models.ResponseOutputTextDoneEvent(
|
|
753
|
+
type="response.output_text.done",
|
|
754
|
+
sequence_number=event_sequence,
|
|
755
|
+
item_id=message_item_id,
|
|
756
|
+
output_index=message_output_index,
|
|
757
|
+
content_index=content_index,
|
|
758
|
+
text=final_text,
|
|
759
|
+
logprobs=logprobs_value,
|
|
760
|
+
)
|
|
761
|
+
)
|
|
762
|
+
message_text_done_emitted = True
|
|
763
|
+
|
|
764
|
+
if message_content_part_added and not message_part_done_emitted:
|
|
765
|
+
sequence_counter += 1
|
|
766
|
+
event_sequence = sequence_counter
|
|
767
|
+
events.append(
|
|
768
|
+
openai_models.ResponseContentPartDoneEvent(
|
|
769
|
+
type="response.content_part.done",
|
|
770
|
+
sequence_number=event_sequence,
|
|
771
|
+
item_id=message_item_id,
|
|
772
|
+
output_index=message_output_index,
|
|
773
|
+
content_index=content_index,
|
|
774
|
+
part=openai_models.ContentPart(
|
|
775
|
+
type="output_text",
|
|
776
|
+
text=final_text,
|
|
777
|
+
annotations=[],
|
|
778
|
+
),
|
|
779
|
+
)
|
|
780
|
+
)
|
|
781
|
+
message_part_done_emitted = True
|
|
782
|
+
|
|
783
|
+
if not message_item_done_emitted:
|
|
784
|
+
sequence_counter += 1
|
|
785
|
+
event_sequence = sequence_counter
|
|
786
|
+
if primary_text_part is None:
|
|
787
|
+
primary_text_part = openai_models.OutputTextContent(
|
|
788
|
+
type="output_text",
|
|
789
|
+
text=final_text,
|
|
790
|
+
annotations=[],
|
|
791
|
+
logprobs=logprobs_value if logprobs_value != [] else [],
|
|
792
|
+
)
|
|
793
|
+
tool_and_aux_blocks.insert(0, primary_text_part)
|
|
794
|
+
message_output = openai_models.MessageOutput(
|
|
795
|
+
type="message",
|
|
796
|
+
id=message_item_id,
|
|
797
|
+
status="completed",
|
|
798
|
+
role="assistant",
|
|
799
|
+
content=tool_and_aux_blocks,
|
|
800
|
+
)
|
|
801
|
+
message_completed_entry = (message_output_index, message_output)
|
|
802
|
+
events.append(
|
|
803
|
+
openai_models.ResponseOutputItemDoneEvent(
|
|
804
|
+
type="response.output_item.done",
|
|
805
|
+
sequence_number=event_sequence,
|
|
806
|
+
output_index=message_output_index,
|
|
807
|
+
item=openai_models.OutputItem(
|
|
808
|
+
id=message_item_id,
|
|
809
|
+
type="message",
|
|
810
|
+
role="assistant",
|
|
811
|
+
status="completed",
|
|
812
|
+
content=[
|
|
813
|
+
part.model_dump()
|
|
814
|
+
if hasattr(part, "model_dump")
|
|
815
|
+
else part
|
|
816
|
+
for part in tool_and_aux_blocks
|
|
817
|
+
],
|
|
818
|
+
text=final_text or None,
|
|
819
|
+
),
|
|
820
|
+
)
|
|
821
|
+
)
|
|
822
|
+
message_item_done_emitted = True
|
|
823
|
+
else:
|
|
824
|
+
if primary_text_part is None and final_text:
|
|
825
|
+
primary_text_part = openai_models.OutputTextContent(
|
|
826
|
+
type="output_text",
|
|
827
|
+
text=final_text,
|
|
828
|
+
annotations=[],
|
|
829
|
+
logprobs=logprobs_value if logprobs_value != [] else [],
|
|
830
|
+
)
|
|
831
|
+
tool_and_aux_blocks.insert(0, primary_text_part)
|
|
832
|
+
message_completed_entry = (
|
|
833
|
+
message_output_index,
|
|
834
|
+
openai_models.MessageOutput(
|
|
835
|
+
type="message",
|
|
836
|
+
id=message_item_id,
|
|
837
|
+
status="completed",
|
|
838
|
+
role="assistant",
|
|
839
|
+
content=tool_and_aux_blocks,
|
|
840
|
+
),
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
return events
|
|
844
|
+
|
|
845
|
+
def make_response_object(
|
|
846
|
+
*,
|
|
847
|
+
status: str,
|
|
848
|
+
model: str | None,
|
|
849
|
+
usage: openai_models.ResponseUsage | None = None,
|
|
850
|
+
output: list[Any] | None = None,
|
|
851
|
+
parallel_override: bool | None = None,
|
|
852
|
+
reasoning_summary: list[dict[str, Any]] | None = None,
|
|
853
|
+
extra: dict[str, Any] | None = None,
|
|
854
|
+
) -> openai_models.ResponseObject:
|
|
855
|
+
payload = dict(envelope_base_kwargs)
|
|
856
|
+
payload["status"] = status
|
|
857
|
+
payload["model"] = model or payload.get("model") or ""
|
|
858
|
+
payload["output"] = output or []
|
|
859
|
+
payload["usage"] = usage
|
|
860
|
+
payload.setdefault("object", "response")
|
|
861
|
+
payload.setdefault("created_at", int(time.time()))
|
|
862
|
+
if parallel_override is not None:
|
|
863
|
+
payload["parallel_tool_calls"] = parallel_override
|
|
864
|
+
if reasoning_summary is not None:
|
|
865
|
+
reasoning_entry = payload.get("reasoning")
|
|
866
|
+
if isinstance(reasoning_entry, openai_models.Reasoning):
|
|
867
|
+
payload["reasoning"] = reasoning_entry.model_copy(
|
|
868
|
+
update={"summary": reasoning_summary}
|
|
869
|
+
)
|
|
870
|
+
elif isinstance(reasoning_entry, dict):
|
|
871
|
+
payload["reasoning"] = openai_models.Reasoning(
|
|
872
|
+
effort=reasoning_entry.get("effort"),
|
|
873
|
+
summary=reasoning_summary,
|
|
874
|
+
)
|
|
875
|
+
else:
|
|
876
|
+
payload["reasoning"] = openai_models.Reasoning(
|
|
877
|
+
effort=None,
|
|
878
|
+
summary=reasoning_summary,
|
|
879
|
+
)
|
|
880
|
+
if extra:
|
|
881
|
+
payload.update(extra)
|
|
882
|
+
return openai_models.ResponseObject(**payload)
|
|
883
|
+
|
|
884
|
+
try:
|
|
885
|
+
async for raw_event in stream:
|
|
886
|
+
event_type, event_payload = _normalize_anthropic_stream_event(raw_event)
|
|
887
|
+
if not event_type:
|
|
888
|
+
continue
|
|
889
|
+
|
|
890
|
+
accumulator.accumulate(event_type, event_payload)
|
|
891
|
+
|
|
892
|
+
if event_type == "ping":
|
|
893
|
+
continue
|
|
894
|
+
|
|
895
|
+
if event_type == "error":
|
|
896
|
+
continue
|
|
897
|
+
|
|
898
|
+
if event_type == "message_start":
|
|
899
|
+
message = (
|
|
900
|
+
event_payload.get("message", {})
|
|
901
|
+
if isinstance(event_payload, dict)
|
|
902
|
+
else {}
|
|
903
|
+
)
|
|
904
|
+
model_id = str(message.get("model", ""))
|
|
905
|
+
response_id, id_suffix = ensure_identifier(
|
|
906
|
+
"resp", message.get("id")
|
|
907
|
+
)
|
|
908
|
+
envelope_base_kwargs["id"] = response_id
|
|
909
|
+
envelope_base_kwargs.setdefault("object", "response")
|
|
910
|
+
if model_id:
|
|
911
|
+
envelope_base_kwargs["model"] = model_id
|
|
912
|
+
if not message_item_id:
|
|
913
|
+
message_item_id = f"msg_{id_suffix}"
|
|
914
|
+
if not reasoning_item_id:
|
|
915
|
+
reasoning_item_id = f"rs_{id_suffix}"
|
|
916
|
+
|
|
917
|
+
created_at_value = (
|
|
918
|
+
message.get("created_at")
|
|
919
|
+
or message.get("created")
|
|
920
|
+
or int(time.time())
|
|
921
|
+
)
|
|
922
|
+
envelope_base_kwargs["created_at"] = int(created_at_value)
|
|
923
|
+
|
|
924
|
+
sequence_counter += 1
|
|
925
|
+
yield openai_models.ResponseCreatedEvent(
|
|
926
|
+
type="response.created",
|
|
927
|
+
sequence_number=sequence_counter,
|
|
928
|
+
response=make_response_object(
|
|
929
|
+
status="in_progress",
|
|
930
|
+
model=model_id,
|
|
931
|
+
usage=None,
|
|
932
|
+
output=[],
|
|
933
|
+
parallel_override=parallel_setting_initial,
|
|
934
|
+
),
|
|
935
|
+
)
|
|
936
|
+
sequence_counter += 1
|
|
937
|
+
yield openai_models.ResponseInProgressEvent(
|
|
938
|
+
type="response.in_progress",
|
|
939
|
+
sequence_number=sequence_counter,
|
|
940
|
+
response=make_response_object(
|
|
941
|
+
status="in_progress",
|
|
942
|
+
model=model_id,
|
|
943
|
+
usage=latest_usage_model,
|
|
944
|
+
output=[],
|
|
945
|
+
parallel_override=parallel_setting_initial,
|
|
946
|
+
),
|
|
947
|
+
)
|
|
948
|
+
continue
|
|
949
|
+
|
|
950
|
+
if event_type == "content_block_start":
|
|
951
|
+
block_index = int(event_payload.get("index", 0))
|
|
952
|
+
content_block = (
|
|
953
|
+
event_payload.get("content_block", {})
|
|
954
|
+
if isinstance(event_payload, dict)
|
|
955
|
+
else {}
|
|
956
|
+
)
|
|
957
|
+
if (
|
|
958
|
+
isinstance(content_block, dict)
|
|
959
|
+
and content_block.get("type") == "tool_use"
|
|
960
|
+
):
|
|
961
|
+
state = ensure_tool_state(block_index)
|
|
962
|
+
name_value = content_block.get("name")
|
|
963
|
+
if isinstance(name_value, str) and name_value:
|
|
964
|
+
state.name = state.name or name_value
|
|
965
|
+
block_id = content_block.get("id")
|
|
966
|
+
if isinstance(block_id, str) and block_id:
|
|
967
|
+
if not state.call_id:
|
|
968
|
+
state.call_id = block_id
|
|
969
|
+
if not state.item_id:
|
|
970
|
+
state.item_id = block_id
|
|
971
|
+
for event in finalize_message():
|
|
972
|
+
yield event
|
|
973
|
+
for event in emit_tool_item_added(block_index, state):
|
|
974
|
+
yield event
|
|
975
|
+
continue
|
|
976
|
+
|
|
977
|
+
if event_type == "content_block_delta":
|
|
978
|
+
block_index = int(event_payload.get("index", 0))
|
|
979
|
+
block_info = accumulator.get_block_info(block_index)
|
|
980
|
+
if not block_info:
|
|
981
|
+
continue
|
|
982
|
+
_, block_meta = block_info
|
|
983
|
+
delta_payload = event_payload.get("delta")
|
|
984
|
+
|
|
985
|
+
block_type = block_meta.get("type")
|
|
986
|
+
|
|
987
|
+
if block_type == "thinking" and isinstance(delta_payload, dict):
|
|
988
|
+
thinking_text = delta_payload.get("thinking")
|
|
989
|
+
if isinstance(thinking_text, str) and thinking_text:
|
|
990
|
+
signature = block_meta.get("signature")
|
|
991
|
+
for event in emit_reasoning_text_delta(
|
|
992
|
+
thinking_text, signature
|
|
993
|
+
):
|
|
994
|
+
yield event
|
|
995
|
+
continue
|
|
996
|
+
|
|
997
|
+
if block_type == "text" and isinstance(delta_payload, dict):
|
|
998
|
+
text_delta = delta_payload.get("text")
|
|
999
|
+
if isinstance(text_delta, str) and text_delta:
|
|
1000
|
+
for event in emit_message_text_delta(
|
|
1001
|
+
text_delta,
|
|
1002
|
+
logprobs=delta_payload.get("logprobs"),
|
|
1003
|
+
obfuscation=delta_payload.get("obfuscation")
|
|
1004
|
+
or delta_payload.get("obfuscated"),
|
|
1005
|
+
):
|
|
1006
|
+
yield event
|
|
1007
|
+
continue
|
|
1008
|
+
|
|
1009
|
+
if block_type == "tool_use" and isinstance(delta_payload, dict):
|
|
1010
|
+
partial = delta_payload.get("partial_json") or ""
|
|
1011
|
+
if partial:
|
|
1012
|
+
state = ensure_tool_state(block_index)
|
|
1013
|
+
for event in finalize_message():
|
|
1014
|
+
yield event
|
|
1015
|
+
for event in emit_tool_item_added(block_index, state):
|
|
1016
|
+
yield event
|
|
1017
|
+
yield emit_tool_arguments_delta(
|
|
1018
|
+
state,
|
|
1019
|
+
str(partial),
|
|
1020
|
+
)
|
|
1021
|
+
continue
|
|
1022
|
+
|
|
1023
|
+
if event_type == "content_block_stop":
|
|
1024
|
+
block_index = int(event_payload.get("index", 0))
|
|
1025
|
+
block_info = accumulator.get_block_info(block_index)
|
|
1026
|
+
if block_info and block_info[1].get("type") == "tool_use":
|
|
1027
|
+
state = ensure_tool_state(block_index)
|
|
1028
|
+
for event in emit_tool_finalize(block_index, state):
|
|
1029
|
+
yield event
|
|
1030
|
+
continue
|
|
1031
|
+
|
|
1032
|
+
if event_type == "message_delta":
|
|
1033
|
+
delta_payload = (
|
|
1034
|
+
event_payload.get("delta", {})
|
|
1035
|
+
if isinstance(event_payload, dict)
|
|
1036
|
+
else {}
|
|
1037
|
+
)
|
|
1038
|
+
stop_reason = (
|
|
1039
|
+
delta_payload.get("stop_reason")
|
|
1040
|
+
if isinstance(delta_payload, dict)
|
|
1041
|
+
else None
|
|
1042
|
+
)
|
|
1043
|
+
if isinstance(stop_reason, str):
|
|
1044
|
+
final_stop_reason = stop_reason
|
|
1045
|
+
|
|
1046
|
+
usage_payload = (
|
|
1047
|
+
event_payload.get("usage")
|
|
1048
|
+
if isinstance(event_payload, dict)
|
|
1049
|
+
else None
|
|
1050
|
+
)
|
|
1051
|
+
usage_model: anthropic_models.Usage | None = None
|
|
1052
|
+
if usage_payload:
|
|
1053
|
+
try:
|
|
1054
|
+
usage_model = anthropic_models.Usage.model_validate(
|
|
1055
|
+
usage_payload
|
|
1056
|
+
)
|
|
1057
|
+
except ValidationError:
|
|
1058
|
+
usage_model = anthropic_models.Usage(
|
|
1059
|
+
input_tokens=usage_payload.get("input_tokens", 0),
|
|
1060
|
+
output_tokens=usage_payload.get("output_tokens", 0),
|
|
1061
|
+
)
|
|
1062
|
+
elif hasattr(raw_event, "usage") and raw_event.usage is not None:
|
|
1063
|
+
usage_model = raw_event.usage
|
|
1064
|
+
|
|
1065
|
+
if usage_model is not None:
|
|
1066
|
+
latest_usage_model = (
|
|
1067
|
+
convert__anthropic_usage_to_openai_responses__usage(
|
|
1068
|
+
usage_model
|
|
1069
|
+
)
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
sequence_counter += 1
|
|
1073
|
+
yield openai_models.ResponseInProgressEvent(
|
|
1074
|
+
type="response.in_progress",
|
|
1075
|
+
sequence_number=sequence_counter,
|
|
1076
|
+
response=make_response_object(
|
|
1077
|
+
status="in_progress",
|
|
1078
|
+
model=model_id,
|
|
1079
|
+
usage=latest_usage_model,
|
|
1080
|
+
output=[],
|
|
1081
|
+
parallel_override=parallel_setting_initial,
|
|
1082
|
+
),
|
|
1083
|
+
)
|
|
1084
|
+
continue
|
|
1085
|
+
|
|
1086
|
+
if event_type == "message_stop":
|
|
1087
|
+
for event in finalize_reasoning():
|
|
1088
|
+
yield event
|
|
1089
|
+
|
|
1090
|
+
for event in finalize_message():
|
|
1091
|
+
yield event
|
|
1092
|
+
|
|
1093
|
+
for index, state in list(tool_states.items()):
|
|
1094
|
+
for event in emit_tool_finalize(index, state):
|
|
1095
|
+
yield event
|
|
1096
|
+
|
|
1097
|
+
first_completed_entries: list[tuple[int, Any]] = []
|
|
1098
|
+
if reasoning_completed_entry is not None:
|
|
1099
|
+
first_completed_entries.append(reasoning_completed_entry)
|
|
1100
|
+
if message_completed_entry is not None:
|
|
1101
|
+
first_completed_entries.append(message_completed_entry)
|
|
1102
|
+
|
|
1103
|
+
for index, state in tool_states.items():
|
|
1104
|
+
tool_entry = accumulator.get_tool_entry(index)
|
|
1105
|
+
if state.name is None and tool_entry is not None:
|
|
1106
|
+
state.name = tool_entry.get("name") or tool_entry.get(
|
|
1107
|
+
"function", {}
|
|
1108
|
+
).get("name")
|
|
1109
|
+
if state.call_id is None and tool_entry is not None:
|
|
1110
|
+
state.call_id = tool_entry.get("id")
|
|
1111
|
+
if not state.item_id:
|
|
1112
|
+
state.item_id = state.call_id or f"call_{state.index}"
|
|
1113
|
+
|
|
1114
|
+
final_args = state.final_arguments
|
|
1115
|
+
if final_args is None:
|
|
1116
|
+
combined = "".join(state.arguments_parts)
|
|
1117
|
+
if not combined and tool_entry is not None:
|
|
1118
|
+
input_payload = tool_entry.get("input", {}) or {}
|
|
1119
|
+
try:
|
|
1120
|
+
combined = json.dumps(
|
|
1121
|
+
input_payload, ensure_ascii=False
|
|
1122
|
+
)
|
|
1123
|
+
except Exception:
|
|
1124
|
+
combined = json.dumps(input_payload)
|
|
1125
|
+
final_args = combined or ""
|
|
1126
|
+
state.final_arguments = final_args
|
|
1127
|
+
|
|
1128
|
+
first_completed_entries.append(
|
|
1129
|
+
(
|
|
1130
|
+
state.output_index,
|
|
1131
|
+
openai_models.FunctionCallOutput(
|
|
1132
|
+
type="function_call",
|
|
1133
|
+
id=state.item_id,
|
|
1134
|
+
status="completed",
|
|
1135
|
+
name=state.name,
|
|
1136
|
+
call_id=state.call_id,
|
|
1137
|
+
arguments=final_args,
|
|
1138
|
+
),
|
|
1139
|
+
)
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
first_completed_entries.sort(key=lambda item: item[0])
|
|
1143
|
+
completed_outputs = [entry for _, entry in first_completed_entries]
|
|
1144
|
+
|
|
1145
|
+
complete_tool_calls_payload = accumulator.get_complete_tool_calls()
|
|
1146
|
+
parallel_final = parallel_setting_initial or len(tool_states) > 1
|
|
1147
|
+
|
|
1148
|
+
extra_fields: dict[str, Any] | None = None
|
|
1149
|
+
if complete_tool_calls_payload:
|
|
1150
|
+
extra_fields = {"tool_calls": complete_tool_calls_payload}
|
|
1151
|
+
|
|
1152
|
+
status_value = "completed"
|
|
1153
|
+
if final_stop_reason == "max_tokens":
|
|
1154
|
+
status_value = "incomplete"
|
|
1155
|
+
|
|
1156
|
+
completed_response = make_response_object(
|
|
1157
|
+
status=status_value,
|
|
1158
|
+
model=model_id,
|
|
1159
|
+
usage=latest_usage_model,
|
|
1160
|
+
output=completed_outputs,
|
|
1161
|
+
parallel_override=parallel_final,
|
|
1162
|
+
reasoning_summary=reasoning_summary_payload,
|
|
1163
|
+
extra=extra_fields,
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
sequence_counter += 1
|
|
1167
|
+
yield openai_models.ResponseCompletedEvent(
|
|
1168
|
+
type="response.completed",
|
|
1169
|
+
sequence_number=sequence_counter,
|
|
1170
|
+
response=completed_response,
|
|
1171
|
+
)
|
|
1172
|
+
stream_completed = True
|
|
1173
|
+
break
|
|
1174
|
+
|
|
1175
|
+
if not stream_completed:
|
|
1176
|
+
for event in finalize_reasoning():
|
|
1177
|
+
yield event
|
|
1178
|
+
|
|
1179
|
+
for event in finalize_message():
|
|
1180
|
+
yield event
|
|
1181
|
+
|
|
1182
|
+
for index, state in list(tool_states.items()):
|
|
1183
|
+
for event in emit_tool_finalize(index, state):
|
|
1184
|
+
yield event
|
|
1185
|
+
|
|
1186
|
+
if (
|
|
1187
|
+
message_completed_entry is None
|
|
1188
|
+
and message_item_added
|
|
1189
|
+
and message_output_index is not None
|
|
1190
|
+
):
|
|
1191
|
+
final_text = "".join(text_buffer)
|
|
1192
|
+
logprobs_value: Any
|
|
1193
|
+
if message_last_logprobs is None:
|
|
1194
|
+
logprobs_value = []
|
|
1195
|
+
else:
|
|
1196
|
+
logprobs_value = message_last_logprobs
|
|
1197
|
+
content_blocks: list[Any] = []
|
|
1198
|
+
if accumulator.content_blocks:
|
|
1199
|
+
sorted_blocks = sorted(
|
|
1200
|
+
accumulator.content_blocks,
|
|
1201
|
+
key=lambda block: block.get("index", 0),
|
|
1202
|
+
)
|
|
1203
|
+
for block in sorted_blocks:
|
|
1204
|
+
block_type = block.get("type")
|
|
1205
|
+
if block_type == "text":
|
|
1206
|
+
text_value = block.get("text", "")
|
|
1207
|
+
content_blocks.append(
|
|
1208
|
+
openai_models.OutputTextContent(
|
|
1209
|
+
type="output_text",
|
|
1210
|
+
text=text_value,
|
|
1211
|
+
annotations=[],
|
|
1212
|
+
logprobs=logprobs_value if text_value else [],
|
|
1213
|
+
)
|
|
1214
|
+
)
|
|
1215
|
+
else:
|
|
1216
|
+
payload = {
|
|
1217
|
+
k: v for k, v in block.items() if k != "index"
|
|
1218
|
+
}
|
|
1219
|
+
if payload.get("type") == "tool_use":
|
|
1220
|
+
tool_input = payload.get("input")
|
|
1221
|
+
if tool_input is not None:
|
|
1222
|
+
payload.setdefault("arguments", tool_input)
|
|
1223
|
+
content_blocks.append(payload)
|
|
1224
|
+
else:
|
|
1225
|
+
if final_text:
|
|
1226
|
+
content_blocks.append(
|
|
1227
|
+
openai_models.OutputTextContent(
|
|
1228
|
+
type="output_text",
|
|
1229
|
+
text=final_text,
|
|
1230
|
+
annotations=[],
|
|
1231
|
+
logprobs=logprobs_value
|
|
1232
|
+
if logprobs_value != []
|
|
1233
|
+
else [],
|
|
1234
|
+
)
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
message_completed_entry = (
|
|
1238
|
+
message_output_index,
|
|
1239
|
+
openai_models.MessageOutput(
|
|
1240
|
+
type="message",
|
|
1241
|
+
id=message_item_id,
|
|
1242
|
+
status="completed",
|
|
1243
|
+
role="assistant",
|
|
1244
|
+
content=content_blocks,
|
|
1245
|
+
),
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
final_completed_entries: list[tuple[int, Any]] = []
|
|
1249
|
+
if reasoning_completed_entry is not None:
|
|
1250
|
+
final_completed_entries.append(reasoning_completed_entry)
|
|
1251
|
+
if message_completed_entry is not None:
|
|
1252
|
+
final_completed_entries.append(message_completed_entry)
|
|
1253
|
+
|
|
1254
|
+
for index, state in tool_states.items():
|
|
1255
|
+
tool_entry = accumulator.get_tool_entry(index)
|
|
1256
|
+
if state.name is None and tool_entry is not None:
|
|
1257
|
+
state.name = tool_entry.get("name") or tool_entry.get(
|
|
1258
|
+
"function", {}
|
|
1259
|
+
).get("name")
|
|
1260
|
+
if state.call_id is None and tool_entry is not None:
|
|
1261
|
+
state.call_id = tool_entry.get("id")
|
|
1262
|
+
if not state.item_id:
|
|
1263
|
+
state.item_id = state.call_id or f"call_{state.index}"
|
|
1264
|
+
final_args = state.final_arguments
|
|
1265
|
+
if final_args is None:
|
|
1266
|
+
combined = "".join(state.arguments_parts)
|
|
1267
|
+
if not combined and tool_entry is not None:
|
|
1268
|
+
input_payload = tool_entry.get("input", {}) or {}
|
|
1269
|
+
try:
|
|
1270
|
+
combined = json.dumps(input_payload, ensure_ascii=False)
|
|
1271
|
+
except Exception:
|
|
1272
|
+
combined = json.dumps(input_payload)
|
|
1273
|
+
final_args = combined or ""
|
|
1274
|
+
state.final_arguments = final_args
|
|
1275
|
+
final_completed_entries.append(
|
|
1276
|
+
(
|
|
1277
|
+
state.output_index,
|
|
1278
|
+
openai_models.FunctionCallOutput(
|
|
1279
|
+
type="function_call",
|
|
1280
|
+
id=state.item_id,
|
|
1281
|
+
status="completed",
|
|
1282
|
+
name=state.name,
|
|
1283
|
+
call_id=state.call_id,
|
|
1284
|
+
arguments=final_args,
|
|
1285
|
+
),
|
|
1286
|
+
)
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
final_completed_entries.sort(key=lambda item: item[0])
|
|
1290
|
+
completed_outputs = [entry for _, entry in final_completed_entries]
|
|
1291
|
+
|
|
1292
|
+
complete_tool_calls_payload = accumulator.get_complete_tool_calls()
|
|
1293
|
+
parallel_final = parallel_setting_initial or len(tool_states) > 1
|
|
1294
|
+
|
|
1295
|
+
final_extra_fields: dict[str, Any] | None = None
|
|
1296
|
+
if complete_tool_calls_payload:
|
|
1297
|
+
final_extra_fields = {"tool_calls": complete_tool_calls_payload}
|
|
1298
|
+
|
|
1299
|
+
fallback_response = make_response_object(
|
|
1300
|
+
status="completed",
|
|
1301
|
+
model=model_id,
|
|
1302
|
+
usage=latest_usage_model,
|
|
1303
|
+
output=completed_outputs,
|
|
1304
|
+
parallel_override=parallel_final,
|
|
1305
|
+
reasoning_summary=reasoning_summary_payload,
|
|
1306
|
+
extra=final_extra_fields,
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
sequence_counter += 1
|
|
1310
|
+
yield openai_models.ResponseCompletedEvent(
|
|
1311
|
+
type="response.completed",
|
|
1312
|
+
sequence_number=sequence_counter,
|
|
1313
|
+
response=fallback_response,
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
finally:
|
|
1317
|
+
register_request(None)
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
class AnthropicToOpenAIChatStreamAdapter:
|
|
1321
|
+
"""Stateful adapter for Anthropic → OpenAI Chat streaming."""
|
|
1322
|
+
|
|
1323
|
+
async def run(
|
|
1324
|
+
self,
|
|
1325
|
+
stream: AsyncIterator[anthropic_models.MessageStreamEvent],
|
|
1326
|
+
) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
|
|
1327
|
+
async for chunk in self._convert_chat_stream(stream):
|
|
1328
|
+
yield chunk
|
|
1329
|
+
|
|
1330
|
+
def _convert_chat_stream(
|
|
1331
|
+
self,
|
|
1332
|
+
stream: AsyncIterator[anthropic_models.MessageStreamEvent],
|
|
1333
|
+
) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
|
|
1334
|
+
"""Convert Anthropic stream to OpenAI stream using ClaudeAccumulator."""
|
|
1335
|
+
|
|
1336
|
+
async def generator() -> AsyncGenerator[
|
|
1337
|
+
openai_models.ChatCompletionChunk, None
|
|
1338
|
+
]:
|
|
1339
|
+
accumulator = ClaudeAccumulator()
|
|
1340
|
+
model_id = ""
|
|
1341
|
+
finish_reason: FinishReason = "stop"
|
|
1342
|
+
usage_prompt = 0
|
|
1343
|
+
usage_completion = 0
|
|
1344
|
+
message_started = False
|
|
1345
|
+
emitted_tool_indices: set[int] = set()
|
|
1346
|
+
|
|
1347
|
+
async for raw_event in stream:
|
|
1348
|
+
event_type, event_payload = _normalize_anthropic_stream_event(raw_event)
|
|
1349
|
+
if not event_type:
|
|
1350
|
+
continue
|
|
1351
|
+
|
|
1352
|
+
accumulator.accumulate(event_type, event_payload)
|
|
1353
|
+
|
|
1354
|
+
if event_type == "ping":
|
|
1355
|
+
continue
|
|
1356
|
+
|
|
1357
|
+
if event_type == "error":
|
|
1358
|
+
# Error events are handled elsewhere by callers.
|
|
1359
|
+
continue
|
|
1360
|
+
|
|
1361
|
+
if event_type == "message_start":
|
|
1362
|
+
message_data = (
|
|
1363
|
+
event_payload.get("message", {})
|
|
1364
|
+
if isinstance(event_payload, dict)
|
|
1365
|
+
else {}
|
|
1366
|
+
)
|
|
1367
|
+
model_id = str(message_data.get("model", ""))
|
|
1368
|
+
message_started = True
|
|
1369
|
+
yield openai_models.ChatCompletionChunk(
|
|
1370
|
+
id="chatcmpl-stream",
|
|
1371
|
+
object="chat.completion.chunk",
|
|
1372
|
+
created=0,
|
|
1373
|
+
model=model_id,
|
|
1374
|
+
choices=[
|
|
1375
|
+
openai_models.StreamingChoice(
|
|
1376
|
+
index=0,
|
|
1377
|
+
delta=openai_models.DeltaMessage(
|
|
1378
|
+
role="assistant", content=""
|
|
1379
|
+
),
|
|
1380
|
+
finish_reason=None,
|
|
1381
|
+
)
|
|
1382
|
+
],
|
|
1383
|
+
)
|
|
1384
|
+
continue
|
|
1385
|
+
|
|
1386
|
+
if not message_started:
|
|
1387
|
+
continue
|
|
1388
|
+
|
|
1389
|
+
if event_type == "content_block_delta":
|
|
1390
|
+
block_index = int(event_payload.get("index", 0))
|
|
1391
|
+
text_delta = _anthropic_delta_to_text(
|
|
1392
|
+
accumulator,
|
|
1393
|
+
block_index,
|
|
1394
|
+
cast(dict[str, Any] | None, event_payload.get("delta")),
|
|
1395
|
+
)
|
|
1396
|
+
if text_delta:
|
|
1397
|
+
yield openai_models.ChatCompletionChunk(
|
|
1398
|
+
id="chatcmpl-stream",
|
|
1399
|
+
object="chat.completion.chunk",
|
|
1400
|
+
created=0,
|
|
1401
|
+
model=model_id,
|
|
1402
|
+
choices=[
|
|
1403
|
+
openai_models.StreamingChoice(
|
|
1404
|
+
index=0,
|
|
1405
|
+
delta=openai_models.DeltaMessage(
|
|
1406
|
+
role="assistant", content=text_delta
|
|
1407
|
+
),
|
|
1408
|
+
finish_reason=None,
|
|
1409
|
+
)
|
|
1410
|
+
],
|
|
1411
|
+
)
|
|
1412
|
+
continue
|
|
1413
|
+
|
|
1414
|
+
if event_type == "content_block_stop":
|
|
1415
|
+
block_index = int(event_payload.get("index", 0))
|
|
1416
|
+
block_info = accumulator.get_block_info(block_index)
|
|
1417
|
+
if not block_info:
|
|
1418
|
+
continue
|
|
1419
|
+
_, block_meta = block_info
|
|
1420
|
+
if block_meta.get("type") != "tool_use":
|
|
1421
|
+
continue
|
|
1422
|
+
if block_index in emitted_tool_indices:
|
|
1423
|
+
continue
|
|
1424
|
+
tool_call = _build_openai_tool_call(accumulator, block_index)
|
|
1425
|
+
if tool_call is None:
|
|
1426
|
+
continue
|
|
1427
|
+
emitted_tool_indices.add(block_index)
|
|
1428
|
+
yield openai_models.ChatCompletionChunk(
|
|
1429
|
+
id="chatcmpl-stream",
|
|
1430
|
+
object="chat.completion.chunk",
|
|
1431
|
+
created=0,
|
|
1432
|
+
model=model_id,
|
|
1433
|
+
choices=[
|
|
1434
|
+
openai_models.StreamingChoice(
|
|
1435
|
+
index=0,
|
|
1436
|
+
delta=openai_models.DeltaMessage(
|
|
1437
|
+
role="assistant", tool_calls=[tool_call]
|
|
1438
|
+
),
|
|
1439
|
+
finish_reason=None,
|
|
1440
|
+
)
|
|
1441
|
+
],
|
|
1442
|
+
)
|
|
1443
|
+
continue
|
|
1444
|
+
|
|
1445
|
+
if event_type == "message_delta":
|
|
1446
|
+
delta_payload = (
|
|
1447
|
+
event_payload.get("delta", {})
|
|
1448
|
+
if isinstance(event_payload, dict)
|
|
1449
|
+
else {}
|
|
1450
|
+
)
|
|
1451
|
+
stop_reason = (
|
|
1452
|
+
delta_payload.get("stop_reason")
|
|
1453
|
+
if isinstance(delta_payload, dict)
|
|
1454
|
+
else None
|
|
1455
|
+
)
|
|
1456
|
+
if isinstance(stop_reason, str):
|
|
1457
|
+
finish_reason = cast(
|
|
1458
|
+
FinishReason,
|
|
1459
|
+
ANTHROPIC_TO_OPENAI_FINISH_REASON.get(stop_reason, "stop"),
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
usage_payload = (
|
|
1463
|
+
event_payload.get("usage")
|
|
1464
|
+
if isinstance(event_payload, dict)
|
|
1465
|
+
else None
|
|
1466
|
+
)
|
|
1467
|
+
if usage_payload:
|
|
1468
|
+
snapshot = anthropic_usage_snapshot(usage_payload)
|
|
1469
|
+
usage_prompt = snapshot.input_tokens
|
|
1470
|
+
usage_completion = snapshot.output_tokens
|
|
1471
|
+
elif hasattr(raw_event, "usage") and raw_event.usage is not None:
|
|
1472
|
+
snapshot = anthropic_usage_snapshot(raw_event.usage)
|
|
1473
|
+
usage_prompt = snapshot.input_tokens
|
|
1474
|
+
usage_completion = snapshot.output_tokens
|
|
1475
|
+
continue
|
|
1476
|
+
|
|
1477
|
+
if event_type == "message_stop":
|
|
1478
|
+
usage = None
|
|
1479
|
+
if usage_prompt or usage_completion:
|
|
1480
|
+
usage = openai_models.CompletionUsage(
|
|
1481
|
+
prompt_tokens=usage_prompt,
|
|
1482
|
+
completion_tokens=usage_completion,
|
|
1483
|
+
total_tokens=usage_prompt + usage_completion,
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
yield openai_models.ChatCompletionChunk(
|
|
1487
|
+
id="chatcmpl-stream",
|
|
1488
|
+
object="chat.completion.chunk",
|
|
1489
|
+
created=0,
|
|
1490
|
+
model=model_id,
|
|
1491
|
+
choices=[
|
|
1492
|
+
openai_models.StreamingChoice(
|
|
1493
|
+
index=0,
|
|
1494
|
+
delta=openai_models.DeltaMessage(),
|
|
1495
|
+
finish_reason=finish_reason,
|
|
1496
|
+
)
|
|
1497
|
+
],
|
|
1498
|
+
usage=usage,
|
|
1499
|
+
)
|
|
1500
|
+
break
|
|
1501
|
+
|
|
1502
|
+
else:
|
|
1503
|
+
if message_started:
|
|
1504
|
+
yield openai_models.ChatCompletionChunk(
|
|
1505
|
+
id="chatcmpl-stream",
|
|
1506
|
+
object="chat.completion.chunk",
|
|
1507
|
+
created=0,
|
|
1508
|
+
model=model_id,
|
|
1509
|
+
choices=[
|
|
1510
|
+
openai_models.StreamingChoice(
|
|
1511
|
+
index=0,
|
|
1512
|
+
delta=openai_models.DeltaMessage(),
|
|
1513
|
+
finish_reason=finish_reason,
|
|
1514
|
+
)
|
|
1515
|
+
],
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
return generator()
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
async def convert__anthropic_message_to_openai_responses__stream(
|
|
1522
|
+
stream: AsyncIterator[anthropic_models.MessageStreamEvent],
|
|
1523
|
+
) -> AsyncGenerator[openai_models.StreamEventType, None]:
|
|
1524
|
+
"""Convert Anthropic MessageStreamEvents into OpenAI Responses stream events."""
|
|
1525
|
+
|
|
1526
|
+
adapter = AnthropicToOpenAIResponsesStreamAdapter()
|
|
1527
|
+
async for event in adapter.run(stream):
|
|
1528
|
+
yield event
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
async def convert__anthropic_message_to_openai_chat__stream(
|
|
1532
|
+
stream: AsyncIterator[anthropic_models.MessageStreamEvent],
|
|
1533
|
+
) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
|
|
1534
|
+
"""Convert Anthropic stream to OpenAI stream using ClaudeAccumulator."""
|
|
1535
|
+
|
|
1536
|
+
adapter = AnthropicToOpenAIChatStreamAdapter()
|
|
1537
|
+
async for chunk in adapter.run(stream):
|
|
1538
|
+
yield chunk
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
__all__ = [
|
|
1542
|
+
"AnthropicToOpenAIChatStreamAdapter",
|
|
1543
|
+
"AnthropicToOpenAIResponsesStreamAdapter",
|
|
1544
|
+
"convert__anthropic_message_to_openai_chat__stream",
|
|
1545
|
+
"convert__anthropic_message_to_openai_responses__stream",
|
|
1546
|
+
]
|