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,1832 @@
|
|
|
1
|
+
"""Streaming conversion entry points for OpenAI↔OpenAI adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
14
|
+
import ccproxy.core.logging
|
|
15
|
+
from ccproxy.llms.formatters.common import (
|
|
16
|
+
THINKING_CLOSE_PATTERN,
|
|
17
|
+
THINKING_OPEN_PATTERN,
|
|
18
|
+
IndexedToolCallTracker,
|
|
19
|
+
ObfuscationTokenFactory,
|
|
20
|
+
ReasoningBuffer,
|
|
21
|
+
ThinkingSegment,
|
|
22
|
+
ToolCallState,
|
|
23
|
+
ToolCallTracker,
|
|
24
|
+
ensure_identifier,
|
|
25
|
+
)
|
|
26
|
+
from ccproxy.llms.formatters.context import (
|
|
27
|
+
get_last_instructions,
|
|
28
|
+
get_last_request,
|
|
29
|
+
get_last_request_tools,
|
|
30
|
+
register_request,
|
|
31
|
+
register_request_tools,
|
|
32
|
+
)
|
|
33
|
+
from ccproxy.llms.models import openai as openai_models
|
|
34
|
+
from ccproxy.llms.streaming.accumulators import OpenAIAccumulator
|
|
35
|
+
|
|
36
|
+
from ._helpers import (
|
|
37
|
+
_convert_tools_chat_to_responses,
|
|
38
|
+
_get_attr,
|
|
39
|
+
)
|
|
40
|
+
from .requests import _build_responses_payload_from_chat_request
|
|
41
|
+
from .responses import (
|
|
42
|
+
_collect_reasoning_segments,
|
|
43
|
+
_wrap_thinking,
|
|
44
|
+
convert__openai_completion_usage_to_openai_responses__usage,
|
|
45
|
+
convert__openai_responses_usage_to_openai_completion__usage,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
logger = ccproxy.core.logging.get_logger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OpenAIResponsesToChatStreamAdapter:
|
|
53
|
+
"""Stateful adapter for Responses -> Chat streaming conversions."""
|
|
54
|
+
|
|
55
|
+
def run(
|
|
56
|
+
self,
|
|
57
|
+
stream: AsyncIterator[openai_models.AnyStreamEvent],
|
|
58
|
+
) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
|
|
59
|
+
"""Convert Response API stream events to ChatCompletionChunk events."""
|
|
60
|
+
|
|
61
|
+
async def generator() -> AsyncGenerator[
|
|
62
|
+
openai_models.ChatCompletionChunk, None
|
|
63
|
+
]:
|
|
64
|
+
model_id = ""
|
|
65
|
+
role_sent = False
|
|
66
|
+
|
|
67
|
+
# Track tool call state keyed by response item id
|
|
68
|
+
tool_tracker = ToolCallTracker()
|
|
69
|
+
tool_delta_emitted = False
|
|
70
|
+
saw_tool_event = False
|
|
71
|
+
tool_candidates: list[tuple[str | None, set[str]]] = []
|
|
72
|
+
reasoning_buffer = ReasoningBuffer()
|
|
73
|
+
|
|
74
|
+
def _extract_tool_signature(tool_entry: Any) -> tuple[str | None, set[str]]:
|
|
75
|
+
name: str | None = None
|
|
76
|
+
param_keys: set[str] = set()
|
|
77
|
+
|
|
78
|
+
if hasattr(tool_entry, "function"):
|
|
79
|
+
fn = getattr(tool_entry, "function", None)
|
|
80
|
+
if fn is not None:
|
|
81
|
+
name = getattr(fn, "name", None)
|
|
82
|
+
parameters = getattr(fn, "parameters", None)
|
|
83
|
+
if isinstance(parameters, dict):
|
|
84
|
+
props = parameters.get("properties")
|
|
85
|
+
if isinstance(props, dict):
|
|
86
|
+
param_keys = {str(key) for key in props}
|
|
87
|
+
if name is None and isinstance(tool_entry, dict):
|
|
88
|
+
fn_dict = tool_entry.get("function")
|
|
89
|
+
if isinstance(fn_dict, dict):
|
|
90
|
+
name = fn_dict.get("name", name)
|
|
91
|
+
parameters = fn_dict.get("parameters")
|
|
92
|
+
if isinstance(parameters, dict):
|
|
93
|
+
props = parameters.get("properties")
|
|
94
|
+
if isinstance(props, dict):
|
|
95
|
+
param_keys = {str(key) for key in props}
|
|
96
|
+
if name is None:
|
|
97
|
+
name = tool_entry.get("name")
|
|
98
|
+
|
|
99
|
+
return name, param_keys
|
|
100
|
+
|
|
101
|
+
def _guess_tool_name(arguments: str | None) -> str | None:
|
|
102
|
+
if not arguments:
|
|
103
|
+
return None
|
|
104
|
+
try:
|
|
105
|
+
parsed = json.loads(arguments)
|
|
106
|
+
except Exception:
|
|
107
|
+
return None
|
|
108
|
+
if not isinstance(parsed, dict):
|
|
109
|
+
return None
|
|
110
|
+
keys = {str(k) for k in parsed}
|
|
111
|
+
if not keys:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
candidates = [
|
|
115
|
+
tool_name
|
|
116
|
+
for tool_name, param_keys in tool_candidates
|
|
117
|
+
if tool_name
|
|
118
|
+
and ((param_keys and keys.issubset(param_keys)) or not param_keys)
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
if len(candidates) == 1:
|
|
122
|
+
return candidates[0]
|
|
123
|
+
|
|
124
|
+
exact = [
|
|
125
|
+
tool_name
|
|
126
|
+
for tool_name, param_keys in tool_candidates
|
|
127
|
+
if tool_name and param_keys == keys
|
|
128
|
+
]
|
|
129
|
+
if len(exact) == 1:
|
|
130
|
+
return exact[0]
|
|
131
|
+
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def _ensure_tool_state(item_id: str) -> ToolCallState:
|
|
135
|
+
return tool_tracker.ensure(item_id)
|
|
136
|
+
|
|
137
|
+
item_id = "msg_stream"
|
|
138
|
+
output_index = 0
|
|
139
|
+
content_index = 0
|
|
140
|
+
sequence_counter = 0
|
|
141
|
+
first_logged = False
|
|
142
|
+
|
|
143
|
+
inline_reasoning_id = "__inline_reasoning__"
|
|
144
|
+
inline_summary_index = "__inline__"
|
|
145
|
+
|
|
146
|
+
async for event_wrapper in stream:
|
|
147
|
+
evt = getattr(event_wrapper, "root", event_wrapper)
|
|
148
|
+
if not hasattr(evt, "type"):
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
logger.debug("stream_event", event_type=getattr(evt, "type", None))
|
|
152
|
+
evt_type = getattr(evt, "type", "")
|
|
153
|
+
|
|
154
|
+
if evt_type == "response.reasoning_summary_part.added":
|
|
155
|
+
item_id = _get_attr(evt, "item_id")
|
|
156
|
+
part = _get_attr(evt, "part")
|
|
157
|
+
if isinstance(item_id, str) and item_id and part is not None:
|
|
158
|
+
summary_index = _get_attr(evt, "summary_index")
|
|
159
|
+
part_signature = _get_attr(part, "signature")
|
|
160
|
+
if isinstance(part_signature, str) and part_signature:
|
|
161
|
+
reasoning_buffer.set_signature(
|
|
162
|
+
item_id, summary_index, part_signature
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
part_type = _get_attr(part, "type")
|
|
166
|
+
part_text = _get_attr(part, "text")
|
|
167
|
+
if (
|
|
168
|
+
part_type == "signature"
|
|
169
|
+
and isinstance(part_text, str)
|
|
170
|
+
and part_text
|
|
171
|
+
):
|
|
172
|
+
reasoning_buffer.set_signature(
|
|
173
|
+
item_id, summary_index, part_text
|
|
174
|
+
)
|
|
175
|
+
reasoning_buffer.reset_buffer(item_id, summary_index)
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
if evt_type in {
|
|
179
|
+
"response.reasoning_summary_text.delta",
|
|
180
|
+
"response.reasoning_text.delta",
|
|
181
|
+
}:
|
|
182
|
+
item_id = _get_attr(evt, "item_id")
|
|
183
|
+
delta_text = _get_attr(evt, "delta")
|
|
184
|
+
if isinstance(item_id, str):
|
|
185
|
+
summary_index = _get_attr(evt, "summary_index")
|
|
186
|
+
reasoning_buffer.append_text(item_id, summary_index, delta_text)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
if evt_type in {
|
|
190
|
+
"response.reasoning_summary_text.done",
|
|
191
|
+
"response.reasoning_text.done",
|
|
192
|
+
}:
|
|
193
|
+
item_id = _get_attr(evt, "item_id")
|
|
194
|
+
text_value = _get_attr(evt, "text")
|
|
195
|
+
if isinstance(item_id, str):
|
|
196
|
+
summary_index = _get_attr(evt, "summary_index")
|
|
197
|
+
for chunk_text in reasoning_buffer.emit(
|
|
198
|
+
item_id, summary_index, text_value
|
|
199
|
+
):
|
|
200
|
+
sequence_counter += 1
|
|
201
|
+
yield openai_models.ChatCompletionChunk(
|
|
202
|
+
id="chatcmpl-stream",
|
|
203
|
+
created=0,
|
|
204
|
+
model=model_id,
|
|
205
|
+
choices=[
|
|
206
|
+
openai_models.StreamingChoice(
|
|
207
|
+
index=0,
|
|
208
|
+
delta=openai_models.DeltaMessage(
|
|
209
|
+
role="assistant" if not role_sent else None,
|
|
210
|
+
content=chunk_text,
|
|
211
|
+
),
|
|
212
|
+
finish_reason=None,
|
|
213
|
+
)
|
|
214
|
+
],
|
|
215
|
+
)
|
|
216
|
+
role_sent = True
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if evt_type == "response.created":
|
|
220
|
+
response_obj = getattr(evt, "response", None)
|
|
221
|
+
model_id = getattr(response_obj, "model", model_id) or model_id
|
|
222
|
+
tools_metadata = getattr(response_obj, "tools", None)
|
|
223
|
+
if not tools_metadata:
|
|
224
|
+
tools_metadata = get_last_request_tools() or []
|
|
225
|
+
if tools_metadata:
|
|
226
|
+
tool_candidates = [
|
|
227
|
+
_extract_tool_signature(entry) for entry in tools_metadata
|
|
228
|
+
]
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
if evt_type == "response.output_text.delta":
|
|
232
|
+
delta_text = getattr(evt, "delta", None) or ""
|
|
233
|
+
if not delta_text:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
remaining = delta_text
|
|
237
|
+
|
|
238
|
+
# Directly create chunks and yield them instead of using a nested function
|
|
239
|
+
# which has closure binding issues
|
|
240
|
+
chunks_to_yield: list[openai_models.ChatCompletionChunk] = []
|
|
241
|
+
|
|
242
|
+
def create_text_chunk(
|
|
243
|
+
current_model_id: str, text_segment: str, is_role_sent: bool
|
|
244
|
+
) -> tuple[openai_models.ChatCompletionChunk | None, bool]:
|
|
245
|
+
if not text_segment:
|
|
246
|
+
return None, is_role_sent
|
|
247
|
+
delta_msg = openai_models.DeltaMessage(
|
|
248
|
+
role="assistant" if not is_role_sent else None,
|
|
249
|
+
content=text_segment,
|
|
250
|
+
)
|
|
251
|
+
new_role_sent = True
|
|
252
|
+
chunk = openai_models.ChatCompletionChunk(
|
|
253
|
+
id="chatcmpl-stream",
|
|
254
|
+
created=0,
|
|
255
|
+
model=current_model_id,
|
|
256
|
+
choices=[
|
|
257
|
+
openai_models.StreamingChoice(
|
|
258
|
+
index=0,
|
|
259
|
+
delta=delta_msg,
|
|
260
|
+
finish_reason=None,
|
|
261
|
+
)
|
|
262
|
+
],
|
|
263
|
+
)
|
|
264
|
+
return chunk, new_role_sent
|
|
265
|
+
|
|
266
|
+
while remaining:
|
|
267
|
+
if reasoning_buffer.is_open(
|
|
268
|
+
inline_reasoning_id, inline_summary_index
|
|
269
|
+
):
|
|
270
|
+
close_match = THINKING_CLOSE_PATTERN.search(remaining)
|
|
271
|
+
if close_match:
|
|
272
|
+
inside_text = remaining[: close_match.start()]
|
|
273
|
+
if inside_text:
|
|
274
|
+
reasoning_buffer.append_text(
|
|
275
|
+
inline_reasoning_id,
|
|
276
|
+
inline_summary_index,
|
|
277
|
+
inside_text,
|
|
278
|
+
)
|
|
279
|
+
for chunk_text in reasoning_buffer.emit(
|
|
280
|
+
inline_reasoning_id, inline_summary_index
|
|
281
|
+
):
|
|
282
|
+
chunk, role_sent = create_text_chunk(
|
|
283
|
+
model_id, chunk_text, role_sent
|
|
284
|
+
)
|
|
285
|
+
if chunk:
|
|
286
|
+
sequence_counter += 1
|
|
287
|
+
chunks_to_yield.append(chunk)
|
|
288
|
+
reasoning_buffer.close_part(
|
|
289
|
+
inline_reasoning_id, inline_summary_index
|
|
290
|
+
)
|
|
291
|
+
remaining = remaining[close_match.end() :]
|
|
292
|
+
continue
|
|
293
|
+
reasoning_buffer.append_text(
|
|
294
|
+
inline_reasoning_id,
|
|
295
|
+
inline_summary_index,
|
|
296
|
+
remaining,
|
|
297
|
+
)
|
|
298
|
+
remaining = ""
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
open_match = THINKING_OPEN_PATTERN.search(remaining)
|
|
302
|
+
if open_match:
|
|
303
|
+
prefix_text = remaining[: open_match.start()]
|
|
304
|
+
if prefix_text:
|
|
305
|
+
chunk, role_sent = create_text_chunk(
|
|
306
|
+
model_id, prefix_text, role_sent
|
|
307
|
+
)
|
|
308
|
+
if chunk:
|
|
309
|
+
sequence_counter += 1
|
|
310
|
+
chunks_to_yield.append(chunk)
|
|
311
|
+
|
|
312
|
+
signature = open_match.group(1) or None
|
|
313
|
+
part_state = reasoning_buffer.ensure_part(
|
|
314
|
+
inline_reasoning_id, inline_summary_index
|
|
315
|
+
)
|
|
316
|
+
if signature:
|
|
317
|
+
part_state.signature = signature
|
|
318
|
+
remaining = remaining[open_match.end() :]
|
|
319
|
+
|
|
320
|
+
if part_state.open:
|
|
321
|
+
# Already inside a reasoning block; ignore duplicate tag
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
reasoning_buffer.open_part(
|
|
325
|
+
inline_reasoning_id, inline_summary_index
|
|
326
|
+
)
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
# No reasoning markers in the rest of the chunk
|
|
330
|
+
if reasoning_buffer.is_open(
|
|
331
|
+
inline_reasoning_id, inline_summary_index
|
|
332
|
+
):
|
|
333
|
+
reasoning_buffer.append_text(
|
|
334
|
+
inline_reasoning_id, inline_summary_index, remaining
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
chunk, role_sent = create_text_chunk(
|
|
338
|
+
model_id, remaining, role_sent
|
|
339
|
+
)
|
|
340
|
+
if chunk:
|
|
341
|
+
sequence_counter += 1
|
|
342
|
+
chunks_to_yield.append(chunk)
|
|
343
|
+
remaining = ""
|
|
344
|
+
|
|
345
|
+
for chunk in chunks_to_yield:
|
|
346
|
+
yield chunk
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
if evt_type == "response.output_item.added":
|
|
350
|
+
item = getattr(evt, "item", None)
|
|
351
|
+
if not item:
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
item_type = getattr(item, "type", None)
|
|
355
|
+
if item_type != "function_call":
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
saw_tool_event = True
|
|
359
|
+
|
|
360
|
+
item_id_value = getattr(item, "id", None) or getattr(
|
|
361
|
+
item, "call_id", None
|
|
362
|
+
)
|
|
363
|
+
if not item_id_value:
|
|
364
|
+
item_id_value = f"call_{uuid.uuid4().hex}"
|
|
365
|
+
item_id = item_id_value
|
|
366
|
+
|
|
367
|
+
state = _ensure_tool_state(item_id)
|
|
368
|
+
state.id = getattr(item, "id", state.id) or state.id
|
|
369
|
+
state.call_id = getattr(item, "call_id", None) or state.call_id
|
|
370
|
+
|
|
371
|
+
if not state.name and state.index < len(tool_candidates):
|
|
372
|
+
candidate_name = tool_candidates[state.index][0]
|
|
373
|
+
if candidate_name:
|
|
374
|
+
state.name = candidate_name
|
|
375
|
+
|
|
376
|
+
name = getattr(item, "name", None)
|
|
377
|
+
if name:
|
|
378
|
+
state.name = name
|
|
379
|
+
|
|
380
|
+
arguments = getattr(item, "arguments", None)
|
|
381
|
+
if isinstance(arguments, str) and arguments:
|
|
382
|
+
state.arguments += arguments
|
|
383
|
+
if not state.name:
|
|
384
|
+
guessed = _guess_tool_name(state.arguments)
|
|
385
|
+
if guessed:
|
|
386
|
+
state.name = guessed
|
|
387
|
+
|
|
388
|
+
# Emit initial tool call chunk to surface id/name information
|
|
389
|
+
if not state.initial_emitted:
|
|
390
|
+
tool_call = openai_models.ToolCall(
|
|
391
|
+
id=state.id,
|
|
392
|
+
type="function",
|
|
393
|
+
function=openai_models.FunctionCall(
|
|
394
|
+
name=state.name or "",
|
|
395
|
+
arguments=arguments or "",
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
state.emitted = True
|
|
399
|
+
state.initial_emitted = True
|
|
400
|
+
if state.name:
|
|
401
|
+
state.name_emitted = True
|
|
402
|
+
if arguments:
|
|
403
|
+
state.arguments_emitted = True
|
|
404
|
+
|
|
405
|
+
tool_delta_emitted = True
|
|
406
|
+
|
|
407
|
+
yield openai_models.ChatCompletionChunk(
|
|
408
|
+
id="chatcmpl-stream",
|
|
409
|
+
created=0,
|
|
410
|
+
model=model_id,
|
|
411
|
+
choices=[
|
|
412
|
+
openai_models.StreamingChoice(
|
|
413
|
+
index=0,
|
|
414
|
+
delta=openai_models.DeltaMessage(
|
|
415
|
+
role="assistant" if not role_sent else None,
|
|
416
|
+
tool_calls=[tool_call],
|
|
417
|
+
),
|
|
418
|
+
finish_reason=None,
|
|
419
|
+
)
|
|
420
|
+
],
|
|
421
|
+
)
|
|
422
|
+
role_sent = True
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
if evt_type == "response.function_call_arguments.delta":
|
|
426
|
+
saw_tool_event = True
|
|
427
|
+
item_id_val = getattr(evt, "item_id", None)
|
|
428
|
+
if not isinstance(item_id_val, str):
|
|
429
|
+
continue
|
|
430
|
+
item_id = item_id_val
|
|
431
|
+
delta_segment = getattr(evt, "delta", None)
|
|
432
|
+
if not isinstance(delta_segment, str):
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
state = _ensure_tool_state(item_id)
|
|
436
|
+
state.arguments += delta_segment
|
|
437
|
+
if not state.name:
|
|
438
|
+
guessed = _guess_tool_name(state.arguments)
|
|
439
|
+
if guessed:
|
|
440
|
+
state.name = guessed
|
|
441
|
+
|
|
442
|
+
if state.initial_emitted:
|
|
443
|
+
tool_call = openai_models.ToolCall(
|
|
444
|
+
id=state.id,
|
|
445
|
+
type="function",
|
|
446
|
+
function=openai_models.FunctionCall(
|
|
447
|
+
name=state.name or "",
|
|
448
|
+
arguments=delta_segment,
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
state.emitted = True
|
|
453
|
+
if delta_segment:
|
|
454
|
+
state.arguments_emitted = True
|
|
455
|
+
|
|
456
|
+
tool_delta_emitted = True
|
|
457
|
+
|
|
458
|
+
yield openai_models.ChatCompletionChunk(
|
|
459
|
+
id="chatcmpl-stream",
|
|
460
|
+
created=0,
|
|
461
|
+
model=model_id,
|
|
462
|
+
choices=[
|
|
463
|
+
openai_models.StreamingChoice(
|
|
464
|
+
index=0,
|
|
465
|
+
delta=openai_models.DeltaMessage(
|
|
466
|
+
role="assistant" if not role_sent else None,
|
|
467
|
+
tool_calls=[tool_call],
|
|
468
|
+
),
|
|
469
|
+
finish_reason=None,
|
|
470
|
+
)
|
|
471
|
+
],
|
|
472
|
+
)
|
|
473
|
+
role_sent = True
|
|
474
|
+
continue
|
|
475
|
+
|
|
476
|
+
if evt_type == "response.function_call_arguments.done":
|
|
477
|
+
saw_tool_event = True
|
|
478
|
+
item_id_val = getattr(evt, "item_id", None)
|
|
479
|
+
if not isinstance(item_id_val, str):
|
|
480
|
+
continue
|
|
481
|
+
item_id = item_id_val
|
|
482
|
+
arguments = getattr(evt, "arguments", None)
|
|
483
|
+
if not isinstance(arguments, str) or not arguments:
|
|
484
|
+
continue
|
|
485
|
+
|
|
486
|
+
state = _ensure_tool_state(item_id)
|
|
487
|
+
# Only emit a chunk if we never emitted arguments earlier
|
|
488
|
+
if not state.arguments_emitted:
|
|
489
|
+
state.arguments = arguments
|
|
490
|
+
if not state.name:
|
|
491
|
+
guessed = _guess_tool_name(arguments)
|
|
492
|
+
if guessed:
|
|
493
|
+
state.name = guessed
|
|
494
|
+
|
|
495
|
+
tool_call = openai_models.ToolCall(
|
|
496
|
+
id=state.id,
|
|
497
|
+
type="function",
|
|
498
|
+
function=openai_models.FunctionCall(
|
|
499
|
+
name=state.name or "",
|
|
500
|
+
arguments=arguments,
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
state.emitted = True
|
|
505
|
+
state.arguments_emitted = True
|
|
506
|
+
|
|
507
|
+
tool_delta_emitted = True
|
|
508
|
+
|
|
509
|
+
yield openai_models.ChatCompletionChunk(
|
|
510
|
+
id="chatcmpl-stream",
|
|
511
|
+
created=0,
|
|
512
|
+
model=model_id,
|
|
513
|
+
choices=[
|
|
514
|
+
openai_models.StreamingChoice(
|
|
515
|
+
index=0,
|
|
516
|
+
delta=openai_models.DeltaMessage(
|
|
517
|
+
role="assistant" if not role_sent else None,
|
|
518
|
+
tool_calls=[tool_call],
|
|
519
|
+
),
|
|
520
|
+
finish_reason=None,
|
|
521
|
+
)
|
|
522
|
+
],
|
|
523
|
+
)
|
|
524
|
+
role_sent = True
|
|
525
|
+
continue
|
|
526
|
+
|
|
527
|
+
if evt_type == "response.output_item.done":
|
|
528
|
+
item = getattr(evt, "item", None)
|
|
529
|
+
if not item:
|
|
530
|
+
continue
|
|
531
|
+
|
|
532
|
+
item_type = getattr(item, "type", None)
|
|
533
|
+
|
|
534
|
+
if item_type == "reasoning":
|
|
535
|
+
summary_list = getattr(item, "summary", None)
|
|
536
|
+
if isinstance(summary_list, list):
|
|
537
|
+
for entry in summary_list:
|
|
538
|
+
text = _get_attr(entry, "text")
|
|
539
|
+
signature = _get_attr(entry, "signature")
|
|
540
|
+
if isinstance(text, str) and text:
|
|
541
|
+
chunk_text = _wrap_thinking(signature, text)
|
|
542
|
+
sequence_counter += 1
|
|
543
|
+
yield openai_models.ChatCompletionChunk(
|
|
544
|
+
id="chatcmpl-stream",
|
|
545
|
+
created=0,
|
|
546
|
+
model=model_id,
|
|
547
|
+
choices=[
|
|
548
|
+
openai_models.StreamingChoice(
|
|
549
|
+
index=0,
|
|
550
|
+
delta=openai_models.DeltaMessage(
|
|
551
|
+
role="assistant"
|
|
552
|
+
if not role_sent
|
|
553
|
+
else None,
|
|
554
|
+
content=chunk_text,
|
|
555
|
+
),
|
|
556
|
+
finish_reason=None,
|
|
557
|
+
)
|
|
558
|
+
],
|
|
559
|
+
)
|
|
560
|
+
role_sent = True
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
if item_type != "function_call":
|
|
564
|
+
continue
|
|
565
|
+
|
|
566
|
+
saw_tool_event = True
|
|
567
|
+
|
|
568
|
+
item_id_value = getattr(item, "id", None) or getattr(
|
|
569
|
+
item, "call_id", None
|
|
570
|
+
)
|
|
571
|
+
if not isinstance(item_id_value, str) or not item_id_value:
|
|
572
|
+
continue
|
|
573
|
+
item_id = item_id_value
|
|
574
|
+
|
|
575
|
+
state = _ensure_tool_state(item_id)
|
|
576
|
+
name = getattr(item, "name", None)
|
|
577
|
+
if name:
|
|
578
|
+
state.name = name
|
|
579
|
+
arguments = getattr(item, "arguments", None)
|
|
580
|
+
if isinstance(arguments, str) and arguments:
|
|
581
|
+
state.arguments = arguments
|
|
582
|
+
if not state.name:
|
|
583
|
+
guessed = _guess_tool_name(arguments)
|
|
584
|
+
if guessed:
|
|
585
|
+
state.name = guessed
|
|
586
|
+
if not state.arguments_emitted:
|
|
587
|
+
tool_call = openai_models.ToolCall(
|
|
588
|
+
id=state.id,
|
|
589
|
+
type="function",
|
|
590
|
+
function=openai_models.FunctionCall(
|
|
591
|
+
name=state.name or "",
|
|
592
|
+
arguments=arguments,
|
|
593
|
+
),
|
|
594
|
+
)
|
|
595
|
+
state.emitted = True
|
|
596
|
+
state.arguments_emitted = True
|
|
597
|
+
|
|
598
|
+
yield openai_models.ChatCompletionChunk(
|
|
599
|
+
id="chatcmpl-stream",
|
|
600
|
+
created=0,
|
|
601
|
+
model=model_id,
|
|
602
|
+
choices=[
|
|
603
|
+
openai_models.StreamingChoice(
|
|
604
|
+
index=0,
|
|
605
|
+
delta=openai_models.DeltaMessage(
|
|
606
|
+
role="assistant" if not role_sent else None,
|
|
607
|
+
tool_calls=[tool_call],
|
|
608
|
+
),
|
|
609
|
+
finish_reason=None,
|
|
610
|
+
)
|
|
611
|
+
],
|
|
612
|
+
)
|
|
613
|
+
role_sent = True
|
|
614
|
+
|
|
615
|
+
# Emit a patch chunk if the name was never surfaced earlier
|
|
616
|
+
if state.name and not state.name_emitted:
|
|
617
|
+
tool_call = openai_models.ToolCall(
|
|
618
|
+
id=state.id,
|
|
619
|
+
type="function",
|
|
620
|
+
function=openai_models.FunctionCall(
|
|
621
|
+
name=state.name or "",
|
|
622
|
+
arguments="",
|
|
623
|
+
),
|
|
624
|
+
)
|
|
625
|
+
state.name_emitted = True
|
|
626
|
+
|
|
627
|
+
tool_delta_emitted = True
|
|
628
|
+
|
|
629
|
+
yield openai_models.ChatCompletionChunk(
|
|
630
|
+
id="chatcmpl-stream",
|
|
631
|
+
created=0,
|
|
632
|
+
model=model_id,
|
|
633
|
+
choices=[
|
|
634
|
+
openai_models.StreamingChoice(
|
|
635
|
+
index=0,
|
|
636
|
+
delta=openai_models.DeltaMessage(
|
|
637
|
+
role="assistant" if not role_sent else None,
|
|
638
|
+
tool_calls=[tool_call],
|
|
639
|
+
),
|
|
640
|
+
finish_reason=None,
|
|
641
|
+
)
|
|
642
|
+
],
|
|
643
|
+
)
|
|
644
|
+
role_sent = True
|
|
645
|
+
|
|
646
|
+
state.completed = True
|
|
647
|
+
continue
|
|
648
|
+
|
|
649
|
+
if evt_type in {
|
|
650
|
+
"response.completed",
|
|
651
|
+
"response.incomplete",
|
|
652
|
+
"response.failed",
|
|
653
|
+
}:
|
|
654
|
+
usage = None
|
|
655
|
+
response_obj = getattr(evt, "response", None)
|
|
656
|
+
if response_obj and getattr(response_obj, "usage", None):
|
|
657
|
+
usage = (
|
|
658
|
+
convert__openai_responses_usage_to_openai_completion__usage(
|
|
659
|
+
response_obj.usage
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
finish_reason: Literal["stop", "length", "tool_calls"] = "stop"
|
|
664
|
+
if (
|
|
665
|
+
tool_delta_emitted
|
|
666
|
+
or saw_tool_event
|
|
667
|
+
or len(tool_tracker)
|
|
668
|
+
or tool_tracker.any_completed()
|
|
669
|
+
):
|
|
670
|
+
finish_reason = "tool_calls"
|
|
671
|
+
|
|
672
|
+
yield openai_models.ChatCompletionChunk(
|
|
673
|
+
id="chatcmpl-stream",
|
|
674
|
+
created=0,
|
|
675
|
+
model=model_id,
|
|
676
|
+
choices=[
|
|
677
|
+
openai_models.StreamingChoice(
|
|
678
|
+
index=0,
|
|
679
|
+
delta=openai_models.DeltaMessage(),
|
|
680
|
+
finish_reason=finish_reason,
|
|
681
|
+
)
|
|
682
|
+
],
|
|
683
|
+
usage=usage,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
# Cleanup request tool cache context when stream completes
|
|
687
|
+
register_request_tools(None)
|
|
688
|
+
|
|
689
|
+
return generator()
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def convert__openai_responses_to_openai_chat__stream(
|
|
693
|
+
stream: AsyncIterator[openai_models.AnyStreamEvent],
|
|
694
|
+
) -> AsyncGenerator[openai_models.ChatCompletionChunk, None]:
|
|
695
|
+
"""Convert Response API stream events to ChatCompletionChunk events."""
|
|
696
|
+
adapter = OpenAIResponsesToChatStreamAdapter()
|
|
697
|
+
return adapter.run(stream)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class OpenAIChatToResponsesStreamAdapter:
|
|
701
|
+
"""Stateful adapter for Chat -> Responses streaming conversions."""
|
|
702
|
+
|
|
703
|
+
def run(
|
|
704
|
+
self,
|
|
705
|
+
stream: AsyncIterator[openai_models.ChatCompletionChunk | dict[str, Any]],
|
|
706
|
+
) -> AsyncGenerator[openai_models.StreamEventType, None]:
|
|
707
|
+
"""Convert OpenAI ChatCompletionChunk stream to Responses API events.
|
|
708
|
+
|
|
709
|
+
Replays chat deltas as Responses events, including function-call output items
|
|
710
|
+
and argument deltas so partial tool calls stream correctly.
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
async def generator() -> AsyncGenerator[openai_models.StreamEventType, None]:
|
|
714
|
+
log = logger.bind(
|
|
715
|
+
category="formatter", converter="chat_to_responses_stream"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
created_sent = False
|
|
719
|
+
response_id = ""
|
|
720
|
+
id_suffix: str | None = None
|
|
721
|
+
last_model = ""
|
|
722
|
+
sequence_counter = -1
|
|
723
|
+
first_logged = False
|
|
724
|
+
|
|
725
|
+
openai_accumulator = OpenAIAccumulator()
|
|
726
|
+
latest_usage_model: openai_models.ResponseUsage | None = None
|
|
727
|
+
convert_usage = convert__openai_completion_usage_to_openai_responses__usage
|
|
728
|
+
delta_event_cls = openai_models.ResponseFunctionCallArgumentsDeltaEvent
|
|
729
|
+
|
|
730
|
+
instructions_text = get_last_instructions()
|
|
731
|
+
if not instructions_text:
|
|
732
|
+
try:
|
|
733
|
+
from ccproxy.core.request_context import RequestContext
|
|
734
|
+
|
|
735
|
+
ctx = RequestContext.get_current()
|
|
736
|
+
if ctx is not None:
|
|
737
|
+
raw_instr = ctx.metadata.get("instructions")
|
|
738
|
+
if isinstance(raw_instr, str) and raw_instr.strip():
|
|
739
|
+
instructions_text = raw_instr.strip()
|
|
740
|
+
except Exception:
|
|
741
|
+
pass
|
|
742
|
+
instructions_value = instructions_text or None
|
|
743
|
+
|
|
744
|
+
envelope_base_kwargs: dict[str, Any] = {
|
|
745
|
+
"id": response_id,
|
|
746
|
+
"object": "response",
|
|
747
|
+
"created_at": 0,
|
|
748
|
+
"instructions": instructions_value,
|
|
749
|
+
}
|
|
750
|
+
reasoning_summary_payload: list[dict[str, Any]] | None = None
|
|
751
|
+
|
|
752
|
+
last_request = get_last_request()
|
|
753
|
+
chat_request: openai_models.ChatCompletionRequest | None = None
|
|
754
|
+
if isinstance(last_request, openai_models.ChatCompletionRequest):
|
|
755
|
+
chat_request = last_request
|
|
756
|
+
elif isinstance(last_request, dict):
|
|
757
|
+
try:
|
|
758
|
+
chat_request = openai_models.ChatCompletionRequest.model_validate(
|
|
759
|
+
last_request
|
|
760
|
+
)
|
|
761
|
+
except ValidationError:
|
|
762
|
+
chat_request = None
|
|
763
|
+
|
|
764
|
+
base_parallel_tool_calls = True
|
|
765
|
+
text_payload: dict[str, Any] | None = None
|
|
766
|
+
|
|
767
|
+
if chat_request is not None:
|
|
768
|
+
request_payload, _ = _build_responses_payload_from_chat_request(
|
|
769
|
+
chat_request
|
|
770
|
+
)
|
|
771
|
+
base_parallel_tool_calls = bool(
|
|
772
|
+
request_payload.get("parallel_tool_calls", True)
|
|
773
|
+
)
|
|
774
|
+
background_value = request_payload.get("background", None)
|
|
775
|
+
envelope_base_kwargs["background"] = (
|
|
776
|
+
bool(background_value) if background_value is not None else None
|
|
777
|
+
)
|
|
778
|
+
for key in (
|
|
779
|
+
"max_output_tokens",
|
|
780
|
+
"tool_choice",
|
|
781
|
+
"tools",
|
|
782
|
+
"store",
|
|
783
|
+
"service_tier",
|
|
784
|
+
"temperature",
|
|
785
|
+
"prompt_cache_key",
|
|
786
|
+
"top_p",
|
|
787
|
+
"top_logprobs",
|
|
788
|
+
"truncation",
|
|
789
|
+
"metadata",
|
|
790
|
+
"user",
|
|
791
|
+
):
|
|
792
|
+
if key in request_payload:
|
|
793
|
+
envelope_base_kwargs[key] = request_payload[key]
|
|
794
|
+
text_payload = request_payload.get("text")
|
|
795
|
+
reasoning_source = request_payload.get("reasoning")
|
|
796
|
+
reasoning_effort = None
|
|
797
|
+
if isinstance(reasoning_source, dict):
|
|
798
|
+
reasoning_effort = reasoning_source.get("effort")
|
|
799
|
+
if reasoning_effort is None:
|
|
800
|
+
reasoning_effort = getattr(chat_request, "reasoning_effort", None)
|
|
801
|
+
envelope_base_kwargs["reasoning"] = openai_models.Reasoning(
|
|
802
|
+
effort=reasoning_effort,
|
|
803
|
+
summary=None,
|
|
804
|
+
)
|
|
805
|
+
if envelope_base_kwargs.get("tool_choice") is None:
|
|
806
|
+
envelope_base_kwargs["tool_choice"] = (
|
|
807
|
+
chat_request.tool_choice or "auto"
|
|
808
|
+
)
|
|
809
|
+
if envelope_base_kwargs.get("tools") is None and chat_request.tools:
|
|
810
|
+
envelope_base_kwargs["tools"] = _convert_tools_chat_to_responses(
|
|
811
|
+
chat_request.tools
|
|
812
|
+
)
|
|
813
|
+
if envelope_base_kwargs.get("store") is None:
|
|
814
|
+
store_value = getattr(chat_request, "store", None)
|
|
815
|
+
if store_value is not None:
|
|
816
|
+
envelope_base_kwargs["store"] = store_value
|
|
817
|
+
if envelope_base_kwargs.get("temperature") is None:
|
|
818
|
+
temperature_value = getattr(chat_request, "temperature", None)
|
|
819
|
+
if temperature_value is not None:
|
|
820
|
+
envelope_base_kwargs["temperature"] = temperature_value
|
|
821
|
+
if envelope_base_kwargs.get("service_tier") is None:
|
|
822
|
+
service_tier_value = getattr(chat_request, "service_tier", None)
|
|
823
|
+
envelope_base_kwargs["service_tier"] = service_tier_value or "auto"
|
|
824
|
+
if "metadata" not in envelope_base_kwargs:
|
|
825
|
+
envelope_base_kwargs["metadata"] = {}
|
|
826
|
+
register_request_tools(chat_request.tools)
|
|
827
|
+
else:
|
|
828
|
+
envelope_base_kwargs["background"] = envelope_base_kwargs.get(
|
|
829
|
+
"background"
|
|
830
|
+
)
|
|
831
|
+
envelope_base_kwargs["reasoning"] = openai_models.Reasoning(
|
|
832
|
+
effort=None, summary=None
|
|
833
|
+
)
|
|
834
|
+
envelope_base_kwargs.setdefault("metadata", {})
|
|
835
|
+
|
|
836
|
+
if text_payload is None:
|
|
837
|
+
text_payload = {"format": {"type": "text"}}
|
|
838
|
+
else:
|
|
839
|
+
text_payload = dict(text_payload)
|
|
840
|
+
|
|
841
|
+
verbosity_value = None
|
|
842
|
+
if chat_request is not None:
|
|
843
|
+
verbosity_value = getattr(chat_request, "verbosity", None)
|
|
844
|
+
if verbosity_value is not None:
|
|
845
|
+
text_payload["verbosity"] = verbosity_value
|
|
846
|
+
else:
|
|
847
|
+
text_payload.setdefault("verbosity", "low")
|
|
848
|
+
envelope_base_kwargs["text"] = text_payload
|
|
849
|
+
|
|
850
|
+
if "store" not in envelope_base_kwargs:
|
|
851
|
+
envelope_base_kwargs["store"] = True
|
|
852
|
+
if "temperature" not in envelope_base_kwargs:
|
|
853
|
+
envelope_base_kwargs["temperature"] = 1.0
|
|
854
|
+
if "service_tier" not in envelope_base_kwargs:
|
|
855
|
+
envelope_base_kwargs["service_tier"] = "auto"
|
|
856
|
+
if "tool_choice" not in envelope_base_kwargs:
|
|
857
|
+
envelope_base_kwargs["tool_choice"] = "auto"
|
|
858
|
+
if "prompt_cache_key" not in envelope_base_kwargs:
|
|
859
|
+
envelope_base_kwargs["prompt_cache_key"] = None
|
|
860
|
+
if "top_p" not in envelope_base_kwargs:
|
|
861
|
+
envelope_base_kwargs["top_p"] = 1.0
|
|
862
|
+
if "top_logprobs" not in envelope_base_kwargs:
|
|
863
|
+
envelope_base_kwargs["top_logprobs"] = None
|
|
864
|
+
if "truncation" not in envelope_base_kwargs:
|
|
865
|
+
envelope_base_kwargs["truncation"] = None
|
|
866
|
+
if "user" not in envelope_base_kwargs:
|
|
867
|
+
envelope_base_kwargs["user"] = None
|
|
868
|
+
|
|
869
|
+
parallel_setting_initial = bool(base_parallel_tool_calls)
|
|
870
|
+
envelope_base_kwargs["parallel_tool_calls"] = parallel_setting_initial
|
|
871
|
+
|
|
872
|
+
message_item_id = ""
|
|
873
|
+
message_output_index: int | None = None
|
|
874
|
+
content_index = 0
|
|
875
|
+
message_item_added = False
|
|
876
|
+
message_content_part_added = False
|
|
877
|
+
message_text_buffer: list[str] = []
|
|
878
|
+
message_last_logprobs: Any | None = None
|
|
879
|
+
message_text_done_emitted = False
|
|
880
|
+
message_part_done_emitted = False
|
|
881
|
+
message_item_done_emitted = False
|
|
882
|
+
message_completed_entry: tuple[int, openai_models.MessageOutput] | None = (
|
|
883
|
+
None
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
reasoning_item_id = ""
|
|
887
|
+
reasoning_output_index: int | None = None
|
|
888
|
+
reasoning_item_added = False
|
|
889
|
+
reasoning_output_done = False
|
|
890
|
+
reasoning_summary_indices: dict[str, int] = {}
|
|
891
|
+
reasoning_summary_added: set[int] = set()
|
|
892
|
+
reasoning_summary_text_fragments: dict[int, list[str]] = {}
|
|
893
|
+
reasoning_summary_text_done: set[int] = set()
|
|
894
|
+
reasoning_summary_part_done: set[int] = set()
|
|
895
|
+
reasoning_completed_entry: (
|
|
896
|
+
tuple[int, openai_models.ReasoningOutput] | None
|
|
897
|
+
) = None
|
|
898
|
+
next_summary_index = 0
|
|
899
|
+
reasoning_summary_signatures: dict[int, str | None] = {}
|
|
900
|
+
|
|
901
|
+
created_at_value: int | None = None
|
|
902
|
+
|
|
903
|
+
next_output_index = 0
|
|
904
|
+
tool_call_states = IndexedToolCallTracker()
|
|
905
|
+
|
|
906
|
+
obfuscation_factory = ObfuscationTokenFactory(
|
|
907
|
+
lambda: id_suffix or response_id or "stream"
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
def ensure_message_output_item() -> (
|
|
911
|
+
openai_models.ResponseOutputItemAddedEvent | None
|
|
912
|
+
):
|
|
913
|
+
nonlocal message_item_added, message_output_index, next_output_index
|
|
914
|
+
nonlocal sequence_counter
|
|
915
|
+
if message_output_index is None:
|
|
916
|
+
message_output_index = next_output_index
|
|
917
|
+
next_output_index += 1
|
|
918
|
+
if not message_item_added:
|
|
919
|
+
message_item_added = True
|
|
920
|
+
sequence_counter += 1
|
|
921
|
+
return openai_models.ResponseOutputItemAddedEvent(
|
|
922
|
+
type="response.output_item.added",
|
|
923
|
+
sequence_number=sequence_counter,
|
|
924
|
+
output_index=message_output_index,
|
|
925
|
+
item=openai_models.OutputItem(
|
|
926
|
+
id=message_item_id,
|
|
927
|
+
type="message",
|
|
928
|
+
role="assistant",
|
|
929
|
+
status="in_progress",
|
|
930
|
+
content=[],
|
|
931
|
+
),
|
|
932
|
+
)
|
|
933
|
+
return None
|
|
934
|
+
|
|
935
|
+
def ensure_message_content_part() -> (
|
|
936
|
+
openai_models.ResponseContentPartAddedEvent | None
|
|
937
|
+
):
|
|
938
|
+
nonlocal message_content_part_added, sequence_counter
|
|
939
|
+
if message_output_index is None:
|
|
940
|
+
return None
|
|
941
|
+
if not message_content_part_added:
|
|
942
|
+
message_content_part_added = True
|
|
943
|
+
sequence_counter += 1
|
|
944
|
+
return openai_models.ResponseContentPartAddedEvent(
|
|
945
|
+
type="response.content_part.added",
|
|
946
|
+
sequence_number=sequence_counter,
|
|
947
|
+
item_id=message_item_id,
|
|
948
|
+
output_index=message_output_index,
|
|
949
|
+
content_index=content_index,
|
|
950
|
+
part=openai_models.ContentPart(
|
|
951
|
+
type="output_text",
|
|
952
|
+
text="",
|
|
953
|
+
annotations=[],
|
|
954
|
+
),
|
|
955
|
+
)
|
|
956
|
+
return None
|
|
957
|
+
|
|
958
|
+
def emit_message_text_delta(
|
|
959
|
+
delta_text: str,
|
|
960
|
+
*,
|
|
961
|
+
logprobs: Any | None = None,
|
|
962
|
+
obfuscation: str | None = None,
|
|
963
|
+
) -> list[openai_models.StreamEventType]:
|
|
964
|
+
if not isinstance(delta_text, str) or not delta_text:
|
|
965
|
+
return []
|
|
966
|
+
|
|
967
|
+
nonlocal \
|
|
968
|
+
message_last_logprobs, \
|
|
969
|
+
sequence_counter, \
|
|
970
|
+
message_item_done_emitted
|
|
971
|
+
if message_item_done_emitted:
|
|
972
|
+
return []
|
|
973
|
+
|
|
974
|
+
events: list[openai_models.StreamEventType] = []
|
|
975
|
+
|
|
976
|
+
message_event = ensure_message_output_item()
|
|
977
|
+
if message_event is not None:
|
|
978
|
+
events.append(message_event)
|
|
979
|
+
|
|
980
|
+
content_event = ensure_message_content_part()
|
|
981
|
+
if content_event is not None:
|
|
982
|
+
events.append(content_event)
|
|
983
|
+
|
|
984
|
+
sequence_counter += 1
|
|
985
|
+
event_sequence = sequence_counter
|
|
986
|
+
logprobs_value: Any
|
|
987
|
+
if logprobs is None:
|
|
988
|
+
logprobs_value = []
|
|
989
|
+
else:
|
|
990
|
+
logprobs_value = logprobs
|
|
991
|
+
obfuscation_value = obfuscation or obfuscation_factory.make(
|
|
992
|
+
"message.delta",
|
|
993
|
+
sequence=event_sequence,
|
|
994
|
+
item_id=message_item_id,
|
|
995
|
+
payload=delta_text,
|
|
996
|
+
)
|
|
997
|
+
events.append(
|
|
998
|
+
openai_models.ResponseOutputTextDeltaEvent(
|
|
999
|
+
type="response.output_text.delta",
|
|
1000
|
+
sequence_number=event_sequence,
|
|
1001
|
+
item_id=message_item_id,
|
|
1002
|
+
output_index=message_output_index or 0,
|
|
1003
|
+
content_index=content_index,
|
|
1004
|
+
delta=delta_text,
|
|
1005
|
+
logprobs=logprobs_value,
|
|
1006
|
+
)
|
|
1007
|
+
)
|
|
1008
|
+
message_text_buffer.append(delta_text)
|
|
1009
|
+
message_last_logprobs = logprobs_value
|
|
1010
|
+
return events
|
|
1011
|
+
|
|
1012
|
+
def _reasoning_key(signature: str | None) -> str:
|
|
1013
|
+
if isinstance(signature, str) and signature.strip():
|
|
1014
|
+
return signature.strip()
|
|
1015
|
+
return "__default__"
|
|
1016
|
+
|
|
1017
|
+
def get_summary_index(signature: str | None) -> int:
|
|
1018
|
+
nonlocal next_summary_index
|
|
1019
|
+
key = _reasoning_key(signature)
|
|
1020
|
+
maybe_index = reasoning_summary_indices.get(key)
|
|
1021
|
+
if maybe_index is not None:
|
|
1022
|
+
return maybe_index
|
|
1023
|
+
reasoning_summary_indices[key] = next_summary_index
|
|
1024
|
+
next_summary_index += 1
|
|
1025
|
+
return reasoning_summary_indices[key]
|
|
1026
|
+
|
|
1027
|
+
def ensure_reasoning_output_item() -> (
|
|
1028
|
+
openai_models.ResponseOutputItemAddedEvent | None
|
|
1029
|
+
):
|
|
1030
|
+
nonlocal reasoning_item_added, reasoning_output_index
|
|
1031
|
+
nonlocal next_output_index, sequence_counter
|
|
1032
|
+
if reasoning_output_index is None:
|
|
1033
|
+
reasoning_output_index = next_output_index
|
|
1034
|
+
next_output_index += 1
|
|
1035
|
+
if not reasoning_item_added:
|
|
1036
|
+
reasoning_item_added = True
|
|
1037
|
+
sequence_counter += 1
|
|
1038
|
+
return openai_models.ResponseOutputItemAddedEvent(
|
|
1039
|
+
type="response.output_item.added",
|
|
1040
|
+
sequence_number=sequence_counter,
|
|
1041
|
+
output_index=reasoning_output_index,
|
|
1042
|
+
item=openai_models.OutputItem(
|
|
1043
|
+
id=reasoning_item_id,
|
|
1044
|
+
type="reasoning",
|
|
1045
|
+
status="in_progress",
|
|
1046
|
+
summary=[],
|
|
1047
|
+
),
|
|
1048
|
+
)
|
|
1049
|
+
return None
|
|
1050
|
+
|
|
1051
|
+
def ensure_reasoning_summary_part(
|
|
1052
|
+
summary_index: int,
|
|
1053
|
+
) -> openai_models.ReasoningSummaryPartAddedEvent | None:
|
|
1054
|
+
nonlocal sequence_counter
|
|
1055
|
+
if reasoning_output_index is None:
|
|
1056
|
+
return None
|
|
1057
|
+
if summary_index in reasoning_summary_added:
|
|
1058
|
+
return None
|
|
1059
|
+
reasoning_summary_added.add(summary_index)
|
|
1060
|
+
sequence_counter += 1
|
|
1061
|
+
return openai_models.ReasoningSummaryPartAddedEvent(
|
|
1062
|
+
type="response.reasoning_summary_part.added",
|
|
1063
|
+
sequence_number=sequence_counter,
|
|
1064
|
+
item_id=reasoning_item_id,
|
|
1065
|
+
output_index=reasoning_output_index,
|
|
1066
|
+
summary_index=summary_index,
|
|
1067
|
+
part=openai_models.ReasoningSummaryPart(
|
|
1068
|
+
type="summary_text",
|
|
1069
|
+
text="",
|
|
1070
|
+
),
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
def emit_reasoning_segments(
|
|
1074
|
+
segments: list[ThinkingSegment],
|
|
1075
|
+
) -> list[openai_models.StreamEventType]:
|
|
1076
|
+
events: list[openai_models.StreamEventType] = []
|
|
1077
|
+
if not segments:
|
|
1078
|
+
return events
|
|
1079
|
+
|
|
1080
|
+
output_event = ensure_reasoning_output_item()
|
|
1081
|
+
if output_event is not None:
|
|
1082
|
+
events.append(output_event)
|
|
1083
|
+
|
|
1084
|
+
nonlocal sequence_counter
|
|
1085
|
+
for segment in segments:
|
|
1086
|
+
text_value = getattr(segment, "thinking", "")
|
|
1087
|
+
if not isinstance(text_value, str) or not text_value:
|
|
1088
|
+
continue
|
|
1089
|
+
summary_index = get_summary_index(
|
|
1090
|
+
getattr(segment, "signature", None)
|
|
1091
|
+
)
|
|
1092
|
+
signature_value = getattr(segment, "signature", None)
|
|
1093
|
+
if summary_index not in reasoning_summary_signatures:
|
|
1094
|
+
reasoning_summary_signatures[summary_index] = signature_value
|
|
1095
|
+
part_event = ensure_reasoning_summary_part(summary_index)
|
|
1096
|
+
if part_event is not None:
|
|
1097
|
+
events.append(part_event)
|
|
1098
|
+
fragments = reasoning_summary_text_fragments.setdefault(
|
|
1099
|
+
summary_index, []
|
|
1100
|
+
)
|
|
1101
|
+
fragments.append(text_value)
|
|
1102
|
+
sequence_counter += 1
|
|
1103
|
+
event_sequence = sequence_counter
|
|
1104
|
+
events.append(
|
|
1105
|
+
openai_models.ReasoningSummaryTextDeltaEvent(
|
|
1106
|
+
type="response.reasoning_summary_text.delta",
|
|
1107
|
+
sequence_number=event_sequence,
|
|
1108
|
+
item_id=reasoning_item_id,
|
|
1109
|
+
output_index=reasoning_output_index or 0,
|
|
1110
|
+
summary_index=summary_index,
|
|
1111
|
+
delta=text_value,
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
return events
|
|
1115
|
+
|
|
1116
|
+
def finalize_reasoning() -> list[openai_models.StreamEventType]:
|
|
1117
|
+
nonlocal reasoning_output_done, reasoning_completed_entry
|
|
1118
|
+
nonlocal reasoning_summary_payload, sequence_counter
|
|
1119
|
+
if not reasoning_item_added or reasoning_output_index is None:
|
|
1120
|
+
return []
|
|
1121
|
+
|
|
1122
|
+
events: list[openai_models.StreamEventType] = []
|
|
1123
|
+
summary_entries: list[dict[str, Any]] = []
|
|
1124
|
+
|
|
1125
|
+
for summary_index in sorted(reasoning_summary_text_fragments):
|
|
1126
|
+
text_value = "".join(
|
|
1127
|
+
reasoning_summary_text_fragments.get(summary_index, [])
|
|
1128
|
+
)
|
|
1129
|
+
if summary_index not in reasoning_summary_text_done:
|
|
1130
|
+
sequence_counter += 1
|
|
1131
|
+
events.append(
|
|
1132
|
+
openai_models.ReasoningSummaryTextDoneEvent(
|
|
1133
|
+
type="response.reasoning_summary_text.done",
|
|
1134
|
+
sequence_number=sequence_counter,
|
|
1135
|
+
item_id=reasoning_item_id,
|
|
1136
|
+
output_index=reasoning_output_index,
|
|
1137
|
+
summary_index=summary_index,
|
|
1138
|
+
text=text_value,
|
|
1139
|
+
)
|
|
1140
|
+
)
|
|
1141
|
+
reasoning_summary_text_done.add(summary_index)
|
|
1142
|
+
if summary_index not in reasoning_summary_part_done:
|
|
1143
|
+
sequence_counter += 1
|
|
1144
|
+
events.append(
|
|
1145
|
+
openai_models.ReasoningSummaryPartDoneEvent(
|
|
1146
|
+
type="response.reasoning_summary_part.done",
|
|
1147
|
+
sequence_number=sequence_counter,
|
|
1148
|
+
item_id=reasoning_item_id,
|
|
1149
|
+
output_index=reasoning_output_index,
|
|
1150
|
+
summary_index=summary_index,
|
|
1151
|
+
part=openai_models.ReasoningSummaryPart(
|
|
1152
|
+
type="summary_text",
|
|
1153
|
+
text=text_value,
|
|
1154
|
+
),
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
reasoning_summary_part_done.add(summary_index)
|
|
1158
|
+
summary_entry: dict[str, Any] = {
|
|
1159
|
+
"type": "summary_text",
|
|
1160
|
+
"text": text_value,
|
|
1161
|
+
}
|
|
1162
|
+
signature_value = reasoning_summary_signatures.get(summary_index)
|
|
1163
|
+
if signature_value:
|
|
1164
|
+
summary_entry["signature"] = signature_value
|
|
1165
|
+
summary_entries.append(summary_entry)
|
|
1166
|
+
|
|
1167
|
+
reasoning_summary_payload = summary_entries
|
|
1168
|
+
|
|
1169
|
+
if not reasoning_output_done:
|
|
1170
|
+
sequence_counter += 1
|
|
1171
|
+
events.append(
|
|
1172
|
+
openai_models.ResponseOutputItemDoneEvent(
|
|
1173
|
+
type="response.output_item.done",
|
|
1174
|
+
sequence_number=sequence_counter,
|
|
1175
|
+
output_index=reasoning_output_index,
|
|
1176
|
+
item=openai_models.OutputItem(
|
|
1177
|
+
id=reasoning_item_id,
|
|
1178
|
+
type="reasoning",
|
|
1179
|
+
status="completed",
|
|
1180
|
+
summary=summary_entries,
|
|
1181
|
+
),
|
|
1182
|
+
)
|
|
1183
|
+
)
|
|
1184
|
+
reasoning_output_done = True
|
|
1185
|
+
reasoning_completed_entry = (
|
|
1186
|
+
reasoning_output_index,
|
|
1187
|
+
openai_models.ReasoningOutput(
|
|
1188
|
+
type="reasoning",
|
|
1189
|
+
id=reasoning_item_id,
|
|
1190
|
+
status="completed",
|
|
1191
|
+
summary=summary_entries,
|
|
1192
|
+
),
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
return events
|
|
1196
|
+
|
|
1197
|
+
def finalize_message() -> list[openai_models.StreamEventType]:
|
|
1198
|
+
nonlocal sequence_counter
|
|
1199
|
+
nonlocal message_text_done_emitted, message_part_done_emitted
|
|
1200
|
+
nonlocal message_item_done_emitted, message_completed_entry
|
|
1201
|
+
nonlocal message_last_logprobs
|
|
1202
|
+
|
|
1203
|
+
if not message_item_added:
|
|
1204
|
+
return []
|
|
1205
|
+
|
|
1206
|
+
events: list[openai_models.StreamEventType] = []
|
|
1207
|
+
final_text = "".join(message_text_buffer)
|
|
1208
|
+
logprobs_value: Any
|
|
1209
|
+
if message_last_logprobs is None:
|
|
1210
|
+
logprobs_value = []
|
|
1211
|
+
else:
|
|
1212
|
+
logprobs_value = message_last_logprobs
|
|
1213
|
+
|
|
1214
|
+
if message_content_part_added and not message_text_done_emitted:
|
|
1215
|
+
sequence_counter += 1
|
|
1216
|
+
event_sequence = sequence_counter
|
|
1217
|
+
events.append(
|
|
1218
|
+
openai_models.ResponseOutputTextDoneEvent(
|
|
1219
|
+
type="response.output_text.done",
|
|
1220
|
+
sequence_number=event_sequence,
|
|
1221
|
+
item_id=message_item_id,
|
|
1222
|
+
output_index=message_output_index or 0,
|
|
1223
|
+
content_index=content_index,
|
|
1224
|
+
text=final_text,
|
|
1225
|
+
logprobs=logprobs_value,
|
|
1226
|
+
)
|
|
1227
|
+
)
|
|
1228
|
+
message_text_done_emitted = True
|
|
1229
|
+
|
|
1230
|
+
if message_content_part_added and not message_part_done_emitted:
|
|
1231
|
+
sequence_counter += 1
|
|
1232
|
+
event_sequence = sequence_counter
|
|
1233
|
+
events.append(
|
|
1234
|
+
openai_models.ResponseContentPartDoneEvent(
|
|
1235
|
+
type="response.content_part.done",
|
|
1236
|
+
sequence_number=event_sequence,
|
|
1237
|
+
item_id=message_item_id,
|
|
1238
|
+
output_index=message_output_index or 0,
|
|
1239
|
+
content_index=content_index,
|
|
1240
|
+
part=openai_models.ContentPart(
|
|
1241
|
+
type="output_text",
|
|
1242
|
+
text=final_text,
|
|
1243
|
+
annotations=[],
|
|
1244
|
+
),
|
|
1245
|
+
)
|
|
1246
|
+
)
|
|
1247
|
+
message_part_done_emitted = True
|
|
1248
|
+
|
|
1249
|
+
if not message_item_done_emitted:
|
|
1250
|
+
sequence_counter += 1
|
|
1251
|
+
event_sequence = sequence_counter
|
|
1252
|
+
output_text_part = openai_models.OutputTextContent(
|
|
1253
|
+
type="output_text",
|
|
1254
|
+
text=final_text,
|
|
1255
|
+
annotations=[],
|
|
1256
|
+
logprobs=logprobs_value if logprobs_value != [] else [],
|
|
1257
|
+
)
|
|
1258
|
+
message_output = openai_models.MessageOutput(
|
|
1259
|
+
type="message",
|
|
1260
|
+
id=message_item_id,
|
|
1261
|
+
status="completed",
|
|
1262
|
+
role="assistant",
|
|
1263
|
+
content=[output_text_part] if final_text else [],
|
|
1264
|
+
)
|
|
1265
|
+
message_completed_entry = (
|
|
1266
|
+
message_output_index or 0,
|
|
1267
|
+
message_output,
|
|
1268
|
+
)
|
|
1269
|
+
events.append(
|
|
1270
|
+
openai_models.ResponseOutputItemDoneEvent(
|
|
1271
|
+
type="response.output_item.done",
|
|
1272
|
+
sequence_number=event_sequence,
|
|
1273
|
+
output_index=message_output_index or 0,
|
|
1274
|
+
item=openai_models.OutputItem(
|
|
1275
|
+
id=message_item_id,
|
|
1276
|
+
type="message",
|
|
1277
|
+
role="assistant",
|
|
1278
|
+
status="completed",
|
|
1279
|
+
content=[output_text_part.model_dump()]
|
|
1280
|
+
if final_text
|
|
1281
|
+
else [],
|
|
1282
|
+
text=final_text or None,
|
|
1283
|
+
),
|
|
1284
|
+
)
|
|
1285
|
+
)
|
|
1286
|
+
message_item_done_emitted = True
|
|
1287
|
+
elif message_completed_entry is None:
|
|
1288
|
+
output_text_part = openai_models.OutputTextContent(
|
|
1289
|
+
type="output_text",
|
|
1290
|
+
text=final_text,
|
|
1291
|
+
annotations=[],
|
|
1292
|
+
logprobs=logprobs_value if logprobs_value != [] else [],
|
|
1293
|
+
)
|
|
1294
|
+
message_completed_entry = (
|
|
1295
|
+
message_output_index or 0,
|
|
1296
|
+
openai_models.MessageOutput(
|
|
1297
|
+
type="message",
|
|
1298
|
+
id=message_item_id,
|
|
1299
|
+
status="completed",
|
|
1300
|
+
role="assistant",
|
|
1301
|
+
content=[output_text_part] if final_text else [],
|
|
1302
|
+
),
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
return events
|
|
1306
|
+
|
|
1307
|
+
def get_tool_state(index: int) -> ToolCallState:
|
|
1308
|
+
nonlocal next_output_index
|
|
1309
|
+
state = tool_call_states.ensure(index)
|
|
1310
|
+
if state.output_index < 0:
|
|
1311
|
+
state.output_index = next_output_index
|
|
1312
|
+
next_output_index += 1
|
|
1313
|
+
return state
|
|
1314
|
+
|
|
1315
|
+
def get_accumulator_entry(idx: int) -> dict[str, Any] | None:
|
|
1316
|
+
for entry in openai_accumulator.tools.values():
|
|
1317
|
+
if entry.get("index") == idx:
|
|
1318
|
+
return entry
|
|
1319
|
+
return None
|
|
1320
|
+
|
|
1321
|
+
def emit_tool_item_added(
|
|
1322
|
+
state: ToolCallState,
|
|
1323
|
+
) -> list[openai_models.StreamEventType]:
|
|
1324
|
+
nonlocal sequence_counter
|
|
1325
|
+
if state.added_emitted:
|
|
1326
|
+
return []
|
|
1327
|
+
if state.name is None:
|
|
1328
|
+
return []
|
|
1329
|
+
if not state.item_id:
|
|
1330
|
+
item_identifier = state.call_id
|
|
1331
|
+
if not item_identifier:
|
|
1332
|
+
item_identifier = f"call_{state.index}"
|
|
1333
|
+
state.item_id = item_identifier
|
|
1334
|
+
sequence_counter += 1
|
|
1335
|
+
state.added_emitted = True
|
|
1336
|
+
return [
|
|
1337
|
+
openai_models.ResponseOutputItemAddedEvent(
|
|
1338
|
+
type="response.output_item.added",
|
|
1339
|
+
sequence_number=sequence_counter,
|
|
1340
|
+
output_index=state.output_index,
|
|
1341
|
+
item=openai_models.OutputItem(
|
|
1342
|
+
id=state.item_id,
|
|
1343
|
+
type="function_call",
|
|
1344
|
+
status="in_progress",
|
|
1345
|
+
name=state.name,
|
|
1346
|
+
arguments="",
|
|
1347
|
+
call_id=state.call_id,
|
|
1348
|
+
),
|
|
1349
|
+
)
|
|
1350
|
+
]
|
|
1351
|
+
|
|
1352
|
+
def finalize_tool_calls() -> list[openai_models.StreamEventType]:
|
|
1353
|
+
nonlocal sequence_counter
|
|
1354
|
+
events: list[openai_models.StreamEventType] = []
|
|
1355
|
+
for idx, state in tool_call_states.items():
|
|
1356
|
+
accumulator_entry = get_accumulator_entry(idx)
|
|
1357
|
+
if state.name is None and accumulator_entry is not None:
|
|
1358
|
+
fn_name = accumulator_entry.get("function", {}).get("name")
|
|
1359
|
+
if isinstance(fn_name, str) and fn_name:
|
|
1360
|
+
state.name = fn_name
|
|
1361
|
+
if state.call_id is None and accumulator_entry is not None:
|
|
1362
|
+
call_identifier = accumulator_entry.get("id")
|
|
1363
|
+
if isinstance(call_identifier, str) and call_identifier:
|
|
1364
|
+
state.call_id = call_identifier
|
|
1365
|
+
if not state.item_id:
|
|
1366
|
+
candidate_id = None
|
|
1367
|
+
if accumulator_entry is not None:
|
|
1368
|
+
candidate_id = accumulator_entry.get("id")
|
|
1369
|
+
state.item_id = (
|
|
1370
|
+
candidate_id or state.call_id or f"call_{state.index}"
|
|
1371
|
+
)
|
|
1372
|
+
if not state.added_emitted:
|
|
1373
|
+
events.extend(emit_tool_item_added(state))
|
|
1374
|
+
final_args = state.final_arguments
|
|
1375
|
+
if final_args is None:
|
|
1376
|
+
combined = "".join(state.arguments_parts or [])
|
|
1377
|
+
if not combined and accumulator_entry is not None:
|
|
1378
|
+
combined = (
|
|
1379
|
+
accumulator_entry.get("function", {}).get("arguments")
|
|
1380
|
+
or ""
|
|
1381
|
+
)
|
|
1382
|
+
final_args = combined or ""
|
|
1383
|
+
state.final_arguments = final_args
|
|
1384
|
+
if not state.arguments_done_emitted:
|
|
1385
|
+
sequence_counter += 1
|
|
1386
|
+
events.append(
|
|
1387
|
+
openai_models.ResponseFunctionCallArgumentsDoneEvent(
|
|
1388
|
+
type="response.function_call_arguments.done",
|
|
1389
|
+
sequence_number=sequence_counter,
|
|
1390
|
+
item_id=state.item_id,
|
|
1391
|
+
output_index=state.output_index,
|
|
1392
|
+
arguments=final_args,
|
|
1393
|
+
)
|
|
1394
|
+
)
|
|
1395
|
+
state.arguments_done_emitted = True
|
|
1396
|
+
if not state.item_done_emitted:
|
|
1397
|
+
sequence_counter += 1
|
|
1398
|
+
events.append(
|
|
1399
|
+
openai_models.ResponseOutputItemDoneEvent(
|
|
1400
|
+
type="response.output_item.done",
|
|
1401
|
+
sequence_number=sequence_counter,
|
|
1402
|
+
output_index=state.output_index,
|
|
1403
|
+
item=openai_models.OutputItem(
|
|
1404
|
+
id=state.item_id,
|
|
1405
|
+
type="function_call",
|
|
1406
|
+
status="completed",
|
|
1407
|
+
name=state.name,
|
|
1408
|
+
arguments=final_args,
|
|
1409
|
+
call_id=state.call_id,
|
|
1410
|
+
),
|
|
1411
|
+
)
|
|
1412
|
+
)
|
|
1413
|
+
state.item_done_emitted = True
|
|
1414
|
+
return events
|
|
1415
|
+
|
|
1416
|
+
def make_response_object(
|
|
1417
|
+
*,
|
|
1418
|
+
status: str,
|
|
1419
|
+
model: str | None,
|
|
1420
|
+
usage: openai_models.ResponseUsage | None = None,
|
|
1421
|
+
output: list[Any] | None = None,
|
|
1422
|
+
parallel_override: bool | None = None,
|
|
1423
|
+
reasoning_summary: list[dict[str, Any]] | None = None,
|
|
1424
|
+
extra: dict[str, Any] | None = None,
|
|
1425
|
+
) -> openai_models.ResponseObject:
|
|
1426
|
+
payload = dict(envelope_base_kwargs)
|
|
1427
|
+
payload["status"] = status
|
|
1428
|
+
payload["model"] = model or payload.get("model") or ""
|
|
1429
|
+
payload["output"] = output or []
|
|
1430
|
+
payload["usage"] = usage
|
|
1431
|
+
payload.setdefault("object", "response")
|
|
1432
|
+
payload.setdefault("created_at", int(time.time()))
|
|
1433
|
+
if parallel_override is not None:
|
|
1434
|
+
payload["parallel_tool_calls"] = parallel_override
|
|
1435
|
+
if reasoning_summary is not None:
|
|
1436
|
+
reasoning_entry = payload.get("reasoning")
|
|
1437
|
+
if isinstance(reasoning_entry, openai_models.Reasoning):
|
|
1438
|
+
payload["reasoning"] = reasoning_entry.model_copy(
|
|
1439
|
+
update={"summary": reasoning_summary}
|
|
1440
|
+
)
|
|
1441
|
+
elif isinstance(reasoning_entry, dict):
|
|
1442
|
+
payload["reasoning"] = openai_models.Reasoning(
|
|
1443
|
+
effort=reasoning_entry.get("effort"),
|
|
1444
|
+
summary=reasoning_summary,
|
|
1445
|
+
)
|
|
1446
|
+
else:
|
|
1447
|
+
payload["reasoning"] = openai_models.Reasoning(
|
|
1448
|
+
effort=None,
|
|
1449
|
+
summary=reasoning_summary,
|
|
1450
|
+
)
|
|
1451
|
+
if extra:
|
|
1452
|
+
payload.update(extra)
|
|
1453
|
+
return openai_models.ResponseObject(**payload)
|
|
1454
|
+
|
|
1455
|
+
try:
|
|
1456
|
+
async for chunk in stream:
|
|
1457
|
+
if isinstance(chunk, dict):
|
|
1458
|
+
chunk_payload = chunk
|
|
1459
|
+
else:
|
|
1460
|
+
chunk_payload = chunk.model_dump(exclude_none=True)
|
|
1461
|
+
|
|
1462
|
+
openai_accumulator.accumulate("", chunk_payload)
|
|
1463
|
+
|
|
1464
|
+
model = chunk_payload.get("model") or last_model
|
|
1465
|
+
choices = chunk_payload.get("choices") or []
|
|
1466
|
+
usage_obj = chunk_payload.get("usage")
|
|
1467
|
+
|
|
1468
|
+
finish_reasons: list[str | None] = []
|
|
1469
|
+
deltas: list[dict[str, Any]] = []
|
|
1470
|
+
for choice in choices:
|
|
1471
|
+
if not isinstance(choice, dict):
|
|
1472
|
+
continue
|
|
1473
|
+
finish_reasons.append(choice.get("finish_reason"))
|
|
1474
|
+
delta_obj = choice.get("delta") or {}
|
|
1475
|
+
if isinstance(delta_obj, dict):
|
|
1476
|
+
deltas.append(delta_obj)
|
|
1477
|
+
|
|
1478
|
+
last_model = model
|
|
1479
|
+
if model:
|
|
1480
|
+
envelope_base_kwargs["model"] = model
|
|
1481
|
+
|
|
1482
|
+
first_delta_text = deltas[0].get("content") if deltas else None
|
|
1483
|
+
|
|
1484
|
+
if not first_logged:
|
|
1485
|
+
first_logged = True
|
|
1486
|
+
with contextlib.suppress(Exception):
|
|
1487
|
+
log.debug(
|
|
1488
|
+
"chat_stream_first_chunk",
|
|
1489
|
+
typed=isinstance(chunk, dict) is False,
|
|
1490
|
+
keys=(
|
|
1491
|
+
list(chunk.keys())
|
|
1492
|
+
if isinstance(chunk, dict)
|
|
1493
|
+
else None
|
|
1494
|
+
),
|
|
1495
|
+
has_delta=bool(first_delta_text),
|
|
1496
|
+
model=model,
|
|
1497
|
+
)
|
|
1498
|
+
if len(choices) == 0 and not model:
|
|
1499
|
+
log.debug("chat_stream_ignoring_first_chunk")
|
|
1500
|
+
continue
|
|
1501
|
+
|
|
1502
|
+
if not created_sent:
|
|
1503
|
+
created_sent = True
|
|
1504
|
+
response_id, id_suffix = ensure_identifier(
|
|
1505
|
+
"resp", chunk_payload.get("id")
|
|
1506
|
+
)
|
|
1507
|
+
envelope_base_kwargs["id"] = response_id
|
|
1508
|
+
envelope_base_kwargs.setdefault("object", "response")
|
|
1509
|
+
if not message_item_id:
|
|
1510
|
+
message_item_id = f"msg_{id_suffix}"
|
|
1511
|
+
if not reasoning_item_id:
|
|
1512
|
+
reasoning_item_id = f"rs_{id_suffix}"
|
|
1513
|
+
|
|
1514
|
+
created_at_value = chunk_payload.get(
|
|
1515
|
+
"created"
|
|
1516
|
+
) or chunk_payload.get("created_at")
|
|
1517
|
+
if created_at_value is None:
|
|
1518
|
+
created_at_value = int(time.time())
|
|
1519
|
+
envelope_base_kwargs["created_at"] = int(created_at_value)
|
|
1520
|
+
|
|
1521
|
+
if model:
|
|
1522
|
+
envelope_base_kwargs["model"] = model
|
|
1523
|
+
elif last_model:
|
|
1524
|
+
envelope_base_kwargs.setdefault("model", last_model)
|
|
1525
|
+
|
|
1526
|
+
sequence_counter += 1
|
|
1527
|
+
response_created = make_response_object(
|
|
1528
|
+
status="in_progress",
|
|
1529
|
+
model=model or last_model,
|
|
1530
|
+
usage=None,
|
|
1531
|
+
output=[],
|
|
1532
|
+
parallel_override=parallel_setting_initial,
|
|
1533
|
+
)
|
|
1534
|
+
yield openai_models.ResponseCreatedEvent(
|
|
1535
|
+
type="response.created",
|
|
1536
|
+
sequence_number=sequence_counter,
|
|
1537
|
+
response=response_created,
|
|
1538
|
+
)
|
|
1539
|
+
sequence_counter += 1
|
|
1540
|
+
yield openai_models.ResponseInProgressEvent(
|
|
1541
|
+
type="response.in_progress",
|
|
1542
|
+
sequence_number=sequence_counter,
|
|
1543
|
+
response=make_response_object(
|
|
1544
|
+
status="in_progress",
|
|
1545
|
+
model=model or last_model,
|
|
1546
|
+
usage=latest_usage_model,
|
|
1547
|
+
output=[],
|
|
1548
|
+
parallel_override=parallel_setting_initial,
|
|
1549
|
+
),
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
for delta in deltas:
|
|
1553
|
+
reasoning_payload = delta.get("reasoning")
|
|
1554
|
+
if reasoning_payload is not None:
|
|
1555
|
+
segments = _collect_reasoning_segments(reasoning_payload)
|
|
1556
|
+
for event in emit_reasoning_segments(segments):
|
|
1557
|
+
yield event
|
|
1558
|
+
|
|
1559
|
+
content_value = delta.get("content")
|
|
1560
|
+
if isinstance(content_value, str) and content_value:
|
|
1561
|
+
for event in emit_message_text_delta(content_value):
|
|
1562
|
+
yield event
|
|
1563
|
+
elif isinstance(content_value, dict):
|
|
1564
|
+
part_type = content_value.get("type")
|
|
1565
|
+
if part_type in {"reasoning", "thinking"}:
|
|
1566
|
+
segments = _collect_reasoning_segments(content_value)
|
|
1567
|
+
for event in emit_reasoning_segments(segments):
|
|
1568
|
+
yield event
|
|
1569
|
+
else:
|
|
1570
|
+
text_value = content_value.get("text")
|
|
1571
|
+
if not isinstance(text_value, str) or not text_value:
|
|
1572
|
+
delta_text = content_value.get("delta")
|
|
1573
|
+
if isinstance(delta_text, str) and delta_text:
|
|
1574
|
+
text_value = delta_text
|
|
1575
|
+
if isinstance(text_value, str) and text_value:
|
|
1576
|
+
for event in emit_message_text_delta(
|
|
1577
|
+
text_value,
|
|
1578
|
+
logprobs=content_value.get("logprobs"),
|
|
1579
|
+
obfuscation=content_value.get("obfuscation")
|
|
1580
|
+
or content_value.get("obfuscated"),
|
|
1581
|
+
):
|
|
1582
|
+
yield event
|
|
1583
|
+
elif isinstance(content_value, list):
|
|
1584
|
+
for part in content_value:
|
|
1585
|
+
if not isinstance(part, dict):
|
|
1586
|
+
continue
|
|
1587
|
+
part_type = part.get("type")
|
|
1588
|
+
if part_type in {"reasoning", "thinking"}:
|
|
1589
|
+
segments = _collect_reasoning_segments(part)
|
|
1590
|
+
for event in emit_reasoning_segments(segments):
|
|
1591
|
+
yield event
|
|
1592
|
+
continue
|
|
1593
|
+
text_value = part.get("text")
|
|
1594
|
+
if not isinstance(text_value, str) or not text_value:
|
|
1595
|
+
delta_text = part.get("delta")
|
|
1596
|
+
if isinstance(delta_text, str) and delta_text:
|
|
1597
|
+
text_value = delta_text
|
|
1598
|
+
if (
|
|
1599
|
+
part_type
|
|
1600
|
+
in {"text", "output_text", "output_text_delta"}
|
|
1601
|
+
and isinstance(text_value, str)
|
|
1602
|
+
and text_value
|
|
1603
|
+
):
|
|
1604
|
+
for event in emit_message_text_delta(
|
|
1605
|
+
text_value,
|
|
1606
|
+
logprobs=part.get("logprobs"),
|
|
1607
|
+
obfuscation=part.get("obfuscation")
|
|
1608
|
+
or part.get("obfuscated"),
|
|
1609
|
+
):
|
|
1610
|
+
yield event
|
|
1611
|
+
|
|
1612
|
+
tool_calls = delta.get("tool_calls") or []
|
|
1613
|
+
if isinstance(tool_calls, list):
|
|
1614
|
+
if tool_calls:
|
|
1615
|
+
for event in finalize_message():
|
|
1616
|
+
yield event
|
|
1617
|
+
for tool_call in tool_calls:
|
|
1618
|
+
if not isinstance(tool_call, dict):
|
|
1619
|
+
continue
|
|
1620
|
+
index_value = int(tool_call.get("index", 0))
|
|
1621
|
+
state = get_tool_state(index_value)
|
|
1622
|
+
tool_id = tool_call.get("id")
|
|
1623
|
+
if isinstance(tool_id, str) and tool_id:
|
|
1624
|
+
state.call_id = tool_id
|
|
1625
|
+
if not state.added_emitted or state.item_id is None:
|
|
1626
|
+
state.item_id = tool_id
|
|
1627
|
+
function_obj = tool_call.get("function") or {}
|
|
1628
|
+
if isinstance(function_obj, dict):
|
|
1629
|
+
name_value = function_obj.get("name")
|
|
1630
|
+
if isinstance(name_value, str) and name_value:
|
|
1631
|
+
state.name = name_value
|
|
1632
|
+
for event in emit_tool_item_added(state):
|
|
1633
|
+
yield event
|
|
1634
|
+
arguments_payload = function_obj.get("arguments")
|
|
1635
|
+
obfuscation_hint = None
|
|
1636
|
+
arguments_delta = ""
|
|
1637
|
+
if isinstance(arguments_payload, str):
|
|
1638
|
+
arguments_delta = arguments_payload
|
|
1639
|
+
elif isinstance(arguments_payload, dict):
|
|
1640
|
+
maybe_delta = arguments_payload.get("delta")
|
|
1641
|
+
if isinstance(maybe_delta, str):
|
|
1642
|
+
arguments_delta = maybe_delta
|
|
1643
|
+
obfuscation_hint = arguments_payload.get(
|
|
1644
|
+
"obfuscation"
|
|
1645
|
+
) or arguments_payload.get("obfuscated")
|
|
1646
|
+
if arguments_delta:
|
|
1647
|
+
state.add_arguments_part(arguments_delta)
|
|
1648
|
+
sequence_counter += 1
|
|
1649
|
+
event_sequence = sequence_counter
|
|
1650
|
+
yield (
|
|
1651
|
+
delta_event_cls(
|
|
1652
|
+
type="response.function_call_arguments.delta",
|
|
1653
|
+
sequence_number=event_sequence,
|
|
1654
|
+
item_id=state.item_id
|
|
1655
|
+
or f"call_{state.index}",
|
|
1656
|
+
output_index=state.output_index,
|
|
1657
|
+
delta=arguments_delta,
|
|
1658
|
+
)
|
|
1659
|
+
)
|
|
1660
|
+
for tool_call in tool_calls:
|
|
1661
|
+
if not isinstance(tool_call, dict):
|
|
1662
|
+
continue
|
|
1663
|
+
index_value = int(tool_call.get("index", 0))
|
|
1664
|
+
state = get_tool_state(index_value)
|
|
1665
|
+
if state.name:
|
|
1666
|
+
for event in emit_tool_item_added(state):
|
|
1667
|
+
yield event
|
|
1668
|
+
|
|
1669
|
+
usage_model: openai_models.ResponseUsage | None = None
|
|
1670
|
+
if usage_obj is not None:
|
|
1671
|
+
try:
|
|
1672
|
+
if isinstance(usage_obj, openai_models.ResponseUsage):
|
|
1673
|
+
usage_model = usage_obj
|
|
1674
|
+
elif isinstance(usage_obj, dict):
|
|
1675
|
+
usage_model = convert_usage(
|
|
1676
|
+
openai_models.CompletionUsage.model_validate(
|
|
1677
|
+
usage_obj
|
|
1678
|
+
)
|
|
1679
|
+
)
|
|
1680
|
+
else:
|
|
1681
|
+
usage_model = convert_usage(usage_obj)
|
|
1682
|
+
except Exception:
|
|
1683
|
+
usage_model = None
|
|
1684
|
+
|
|
1685
|
+
if usage_model is not None:
|
|
1686
|
+
latest_usage_model = usage_model
|
|
1687
|
+
if all(reason is None for reason in finish_reasons):
|
|
1688
|
+
sequence_counter += 1
|
|
1689
|
+
yield openai_models.ResponseInProgressEvent(
|
|
1690
|
+
type="response.in_progress",
|
|
1691
|
+
sequence_number=sequence_counter,
|
|
1692
|
+
response=make_response_object(
|
|
1693
|
+
status="in_progress",
|
|
1694
|
+
model=model or last_model,
|
|
1695
|
+
usage=usage_model,
|
|
1696
|
+
output=[],
|
|
1697
|
+
parallel_override=parallel_setting_initial,
|
|
1698
|
+
),
|
|
1699
|
+
)
|
|
1700
|
+
|
|
1701
|
+
if any(reason == "tool_calls" for reason in finish_reasons):
|
|
1702
|
+
for event in finalize_message():
|
|
1703
|
+
yield event
|
|
1704
|
+
for event in finalize_tool_calls():
|
|
1705
|
+
yield event
|
|
1706
|
+
|
|
1707
|
+
finally:
|
|
1708
|
+
register_request(None)
|
|
1709
|
+
register_request_tools(None)
|
|
1710
|
+
|
|
1711
|
+
for event in finalize_reasoning():
|
|
1712
|
+
yield event
|
|
1713
|
+
|
|
1714
|
+
for event in finalize_message():
|
|
1715
|
+
yield event
|
|
1716
|
+
|
|
1717
|
+
for event in finalize_tool_calls():
|
|
1718
|
+
yield event
|
|
1719
|
+
|
|
1720
|
+
if message_completed_entry is None and message_item_added:
|
|
1721
|
+
final_text = "".join(message_text_buffer)
|
|
1722
|
+
logprobs_value: Any
|
|
1723
|
+
if message_last_logprobs is None:
|
|
1724
|
+
logprobs_value = []
|
|
1725
|
+
else:
|
|
1726
|
+
logprobs_value = message_last_logprobs
|
|
1727
|
+
output_text_part = openai_models.OutputTextContent(
|
|
1728
|
+
type="output_text",
|
|
1729
|
+
text=final_text,
|
|
1730
|
+
annotations=[],
|
|
1731
|
+
logprobs=logprobs_value if logprobs_value != [] else [],
|
|
1732
|
+
)
|
|
1733
|
+
message_completed_entry = (
|
|
1734
|
+
message_output_index or 0,
|
|
1735
|
+
openai_models.MessageOutput(
|
|
1736
|
+
type="message",
|
|
1737
|
+
id=message_item_id,
|
|
1738
|
+
status="completed",
|
|
1739
|
+
role="assistant",
|
|
1740
|
+
content=[output_text_part] if final_text else [],
|
|
1741
|
+
),
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
completed_entries: list[tuple[int, Any]] = []
|
|
1745
|
+
if reasoning_completed_entry is not None:
|
|
1746
|
+
completed_entries.append(reasoning_completed_entry)
|
|
1747
|
+
if message_completed_entry is not None:
|
|
1748
|
+
completed_entries.append(message_completed_entry)
|
|
1749
|
+
|
|
1750
|
+
for idx, state in tool_call_states.items():
|
|
1751
|
+
accumulator_entry = get_accumulator_entry(idx)
|
|
1752
|
+
if state.final_arguments is None:
|
|
1753
|
+
aggregated = ""
|
|
1754
|
+
if accumulator_entry is not None:
|
|
1755
|
+
aggregated = (
|
|
1756
|
+
accumulator_entry.get("function", {}).get("arguments") or ""
|
|
1757
|
+
)
|
|
1758
|
+
if not aggregated:
|
|
1759
|
+
aggregated = "".join(state.arguments_parts or [])
|
|
1760
|
+
state.final_arguments = aggregated or ""
|
|
1761
|
+
if state.name is None and accumulator_entry is not None:
|
|
1762
|
+
fn_name = accumulator_entry.get("function", {}).get("name")
|
|
1763
|
+
if isinstance(fn_name, str) and fn_name:
|
|
1764
|
+
state.name = fn_name
|
|
1765
|
+
if not state.item_id:
|
|
1766
|
+
candidate_id = None
|
|
1767
|
+
if accumulator_entry is not None:
|
|
1768
|
+
candidate_id = accumulator_entry.get("id")
|
|
1769
|
+
state.item_id = candidate_id or f"call_{state.index}"
|
|
1770
|
+
completed_entries.append(
|
|
1771
|
+
(
|
|
1772
|
+
state.output_index,
|
|
1773
|
+
openai_models.FunctionCallOutput(
|
|
1774
|
+
type="function_call",
|
|
1775
|
+
id=state.item_id,
|
|
1776
|
+
status="completed",
|
|
1777
|
+
name=state.name,
|
|
1778
|
+
call_id=state.call_id,
|
|
1779
|
+
arguments=state.final_arguments or "",
|
|
1780
|
+
),
|
|
1781
|
+
)
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
completed_entries.sort(key=lambda item: item[0])
|
|
1785
|
+
completed_outputs = [entry for _, entry in completed_entries]
|
|
1786
|
+
|
|
1787
|
+
complete_tool_calls_payload = openai_accumulator.get_complete_tool_calls()
|
|
1788
|
+
parallel_tool_calls = len(tool_call_states) > 1
|
|
1789
|
+
parallel_final = parallel_tool_calls or parallel_setting_initial
|
|
1790
|
+
|
|
1791
|
+
extra_fields: dict[str, Any] | None = None
|
|
1792
|
+
if complete_tool_calls_payload:
|
|
1793
|
+
extra_fields = {"tool_calls": complete_tool_calls_payload}
|
|
1794
|
+
|
|
1795
|
+
response_completed = make_response_object(
|
|
1796
|
+
status="completed",
|
|
1797
|
+
model=last_model,
|
|
1798
|
+
usage=latest_usage_model,
|
|
1799
|
+
output=completed_outputs,
|
|
1800
|
+
parallel_override=parallel_final,
|
|
1801
|
+
reasoning_summary=reasoning_summary_payload,
|
|
1802
|
+
extra=extra_fields,
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
sequence_counter += 1
|
|
1806
|
+
yield openai_models.ResponseCompletedEvent(
|
|
1807
|
+
type="response.completed",
|
|
1808
|
+
sequence_number=sequence_counter,
|
|
1809
|
+
response=response_completed,
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
return generator()
|
|
1813
|
+
|
|
1814
|
+
|
|
1815
|
+
def convert__openai_chat_to_openai_responses__stream(
|
|
1816
|
+
stream: AsyncIterator[openai_models.ChatCompletionChunk | dict[str, Any]],
|
|
1817
|
+
) -> AsyncGenerator[openai_models.StreamEventType, None]:
|
|
1818
|
+
"""Convert OpenAI ChatCompletionChunk stream to Responses API events.
|
|
1819
|
+
|
|
1820
|
+
Replays chat deltas as Responses events, including function-call output items
|
|
1821
|
+
and argument deltas so partial tool calls stream correctly.
|
|
1822
|
+
"""
|
|
1823
|
+
adapter = OpenAIChatToResponsesStreamAdapter()
|
|
1824
|
+
return adapter.run(stream)
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
__all__ = [
|
|
1828
|
+
"OpenAIChatToResponsesStreamAdapter",
|
|
1829
|
+
"OpenAIResponsesToChatStreamAdapter",
|
|
1830
|
+
"convert__openai_chat_to_openai_responses__stream",
|
|
1831
|
+
"convert__openai_responses_to_openai_chat__stream",
|
|
1832
|
+
]
|